refactor: replace enfeeble with wound/skill rebalancing and add venom/burrow/hive_link support

- Replace enfeeble skill with wound (value adjusted per card) across multiple enemy cards in data/cards.json
- Add venom preBattle skill to several enemy cards (3/6/12 stacks)
- Add burrow all: true skill to multiple enemy cards (1–3 turns)
- Add cleanse and hive_link skills to hive-related enemies with postBattle trigger
- In CombatEngine.js: implement _applyCommanderBurrowBonus for deploying/spawned cards
- Implement commander hive_link all: boost surviving allies' ATK when any card dies
- In SkillProcessor.js: update all all-targeting skills (strike, pierce, mortar, jam, weaken) to skip targets with burrowTurns > 0
- Add venom skill support with stack handling and prevStacks tracking for display restoration
- In BattleScene.js: extend preBattle event handling to include venomFires; fix hive_link deadCardId resolution
This commit is contained in:
Brian Fertig 2026-04-03 22:02:28 -06:00
parent e6049b6985
commit d98070915f
4 changed files with 172 additions and 56 deletions

View File

@ -4181,16 +4181,10 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "enfeeble", "name": "wound",
"value": 1, "value": 1,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
},
{
"name": "weaken",
"value": 2,
"all": true,
"trigger": "preBattle"
} }
] ]
}, },
@ -4201,16 +4195,10 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "enfeeble", "name": "wound",
"value": 2, "value": 2,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
},
{
"name": "weaken",
"value": 3,
"all": true,
"trigger": "preBattle"
} }
] ]
}, },
@ -4221,8 +4209,8 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "enfeeble", "name": "wound",
"value": 3, "value": 5,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
}, },
@ -4241,14 +4229,14 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "enfeeble", "name": "wound",
"value": 5, "value": 8,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
}, },
{ {
"name": "weaken", "name": "weaken",
"value": 7, "value": 8,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
} }
@ -4261,8 +4249,8 @@
"delay": 0, "delay": 0,
"skills": [ "skills": [
{ {
"name": "enfeeble", "name": "wound",
"value": 7, "value": 10,
"all": true, "all": true,
"trigger": "preBattle" "trigger": "preBattle"
}, },
@ -6014,6 +6002,12 @@
"name": "spawn", "name": "spawn",
"value": 1, "value": 1,
"trigger": "on_turn_start" "trigger": "on_turn_start"
},
{
"name": "venom",
"value": 3,
"all": true,
"trigger": "preBattle"
} }
] ]
}, },
@ -6027,6 +6021,12 @@
"name": "spawn", "name": "spawn",
"value": 1, "value": 1,
"trigger": "on_turn_start" "trigger": "on_turn_start"
},
{
"name": "venom",
"value": 6,
"all": true,
"trigger": "preBattle"
} }
] ]
}, },
@ -6040,6 +6040,12 @@
"name": "spawn", "name": "spawn",
"value": 1, "value": 1,
"trigger": "on_turn_start" "trigger": "on_turn_start"
},
{
"name": "venom",
"value": 12,
"all": true,
"trigger": "preBattle"
} }
] ]
} }
@ -6093,6 +6099,11 @@
"value": 3, "value": 3,
"trigger": "preBattle", "trigger": "preBattle",
"all": true "all": true
},
{
"name": "burrow",
"value": 1,
"all": true
} }
] ]
}, },
@ -6107,6 +6118,11 @@
"value": 5, "value": 5,
"trigger": "preBattle", "trigger": "preBattle",
"all": true "all": true
},
{
"name": "burrow",
"value": 2,
"all": true
} }
] ]
}, },
@ -6121,6 +6137,11 @@
"value": 7, "value": 7,
"trigger": "preBattle", "trigger": "preBattle",
"all": true "all": true
},
{
"name": "burrow",
"value": 3,
"all": true
} }
] ]
} }
@ -6145,12 +6166,6 @@
"name": "spawn", "name": "spawn",
"value": 1, "value": 1,
"trigger": "on_turn_start" "trigger": "on_turn_start"
},
{
"name": "enfeeble",
"value": 1,
"trigger": "preBattle",
"all": true
} }
] ]
}, },
@ -6166,10 +6181,10 @@
"trigger": "on_turn_start" "trigger": "on_turn_start"
}, },
{ {
"name": "enfeeble", "name": "cleanse",
"value": 2, "value": 2,
"trigger": "preBattle", "all": true,
"all": true "trigger": "postBattle"
} }
] ]
}, },
@ -6185,9 +6200,14 @@
"trigger": "on_turn_start" "trigger": "on_turn_start"
}, },
{ {
"name": "enfeeble", "name": "cleanse",
"value": 3, "value": 4,
"trigger": "preBattle", "all": true,
"trigger": "postBattle"
},
{
"name": "hive_link",
"value": 2,
"all": true "all": true
} }
] ]
@ -6204,9 +6224,14 @@
"trigger": "on_turn_start" "trigger": "on_turn_start"
}, },
{ {
"name": "enfeeble", "name": "cleanse",
"value": 5, "value": 6,
"trigger": "preBattle", "all": true,
"trigger": "postBattle"
},
{
"name": "hive_link",
"value": 4,
"all": true "all": true
} }
] ]
@ -6223,9 +6248,14 @@
"trigger": "on_turn_start" "trigger": "on_turn_start"
}, },
{ {
"name": "enfeeble", "name": "cleanse",
"value": 7, "value": 8,
"trigger": "preBattle", "all": true,
"trigger": "postBattle"
},
{
"name": "hive_link",
"value": 6,
"all": true "all": true
} }
] ]

View File

@ -44,15 +44,23 @@ export class CombatEngine {
} }
// Deploy one card from hand into next open lane // Deploy one card from hand into next open lane
_applyCommanderBurrowBonus(card, commander) {
const skill = commander?.skills?.find(s => s.name === 'burrow' && s.all);
if (!skill) return;
card.burrowTurns += skill.value;
}
_deployPhase() { _deployPhase() {
if (this.playerHand.length > 0 && this.playerLanes.length < MAX_LANES) { if (this.playerHand.length > 0 && this.playerLanes.length < MAX_LANES) {
const card = this.playerHand.shift(); const card = this.playerHand.shift();
this._applyCommanderBurrowBonus(card, this.playerCommander);
this.playerLanes.push(card); this.playerLanes.push(card);
this._log(`Player deploys ${card.name}`); this._log(`Player deploys ${card.name}`);
this.events.push({ type: 'deploy', side: 'player', card }); this.events.push({ type: 'deploy', side: 'player', card });
} }
const aiCard = this.ai.getNextCard(); const aiCard = this.ai.getNextCard();
if (aiCard && this.opponentLanes.length < MAX_LANES) { if (aiCard && this.opponentLanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard); this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`); this._log(`Opponent deploys ${aiCard.name}`);
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard }); this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
@ -203,6 +211,7 @@ export class CombatEngine {
this.skillProcessor.process(s, commander, null, this.skillProcessor.process(s, commander, null,
[commander, ...lanes], [enemyCommander, ...enemyLanes], ctx); [commander, ...lanes], [enemyCommander, ...enemyLanes], ctx);
if (ctx.spawnedDrone && lanes.length < MAX_LANES) { if (ctx.spawnedDrone && lanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(ctx.spawnedDrone, commander);
lanes.push(ctx.spawnedDrone); lanes.push(ctx.spawnedDrone);
const side = (commander === this.playerCommander) ? 'player' : 'opponent'; const side = (commander === this.playerCommander) ? 'player' : 'opponent';
this._log(`${commander.name} spawns a ${ctx.spawnedDrone.name}`); this._log(`${commander.name} spawns a ${ctx.spawnedDrone.name}`);
@ -420,6 +429,23 @@ export class CombatEngine {
this.events.push({ type: 'hive_link', source: deadCard, targets: ctx.hiveLinkTargets, gain: s.value, side }); this.events.push({ type: 'hive_link', source: deadCard, targets: ctx.hiveLinkTargets, gain: s.value, side });
} }
} }
// Commander hive_link all: boost all surviving allies when any card dies
const alliedCommander = side === 'player' ? this.playerCommander : this.opponentCommander;
const hlAllSkill = alliedCommander?.skills?.find(s => s.name === 'hive_link' && s.all);
if (hlAllSkill) {
const remaining = alliedLanes.filter(a => a.currentHP > 0 && a !== deadCard);
if (remaining.length > 0) {
const targets = [];
for (const ally of remaining) {
ally.currentAttack += hlAllSkill.value;
ally.attack += hlAllSkill.value;
targets.push({ target: ally, gain: hlAllSkill.value });
}
this._log(`${alliedCommander.name} hive_link all: +${hlAllSkill.value} ATK to ${targets.length} allies on death of ${deadCard.name}`);
this.events.push({ type: 'hive_link', source: alliedCommander, deadCardId: deadCard.instanceId, targets, gain: hlAllSkill.value, side });
}
}
} }
// Remove dead cards, compact lanes // Remove dead cards, compact lanes
@ -464,6 +490,7 @@ export class CombatEngine {
// and should not be ticked the same turn it enters the field (same rule as // and should not be ticked the same turn it enters the field (same rule as
// player cards, which deploy after _activationPhase() runs in beginCommit). // player cards, which deploy after _activationPhase() runs in beginCommit).
aiCard._justDeployed = true; aiCard._justDeployed = true;
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard); this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`); this._log(`Opponent deploys ${aiCard.name}`);
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard }); this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
@ -480,6 +507,7 @@ export class CombatEngine {
const idx = this.playerHand.findIndex(c => c.instanceId === chosenCard.instanceId); const idx = this.playerHand.findIndex(c => c.instanceId === chosenCard.instanceId);
if (idx !== -1) { if (idx !== -1) {
this.playerHand.splice(idx, 1); this.playerHand.splice(idx, 1);
this._applyCommanderBurrowBonus(chosenCard, this.playerCommander);
this.playerLanes.push(chosenCard); this.playerLanes.push(chosenCard);
this._log(`Player deploys ${chosenCard.name}`); this._log(`Player deploys ${chosenCard.name}`);
this.events.push({ type: 'deploy', side: 'player', card: chosenCard }); this.events.push({ type: 'deploy', side: 'player', card: chosenCard });
@ -514,6 +542,7 @@ export class CombatEngine {
const idx = this.playerHand.findIndex(c => c.instanceId === chosenCard.instanceId); const idx = this.playerHand.findIndex(c => c.instanceId === chosenCard.instanceId);
if (idx !== -1) { if (idx !== -1) {
this.playerHand.splice(idx, 1); this.playerHand.splice(idx, 1);
this._applyCommanderBurrowBonus(chosenCard, this.playerCommander);
this.playerLanes.push(chosenCard); this.playerLanes.push(chosenCard);
this._log(`Player deploys ${chosenCard.name}`); this._log(`Player deploys ${chosenCard.name}`);
this.events.push({ type: 'deploy', side: 'player', card: chosenCard }); this.events.push({ type: 'deploy', side: 'player', card: chosenCard });
@ -526,6 +555,7 @@ export class CombatEngine {
if (this.playerGoesFirst) { if (this.playerGoesFirst) {
const aiCard = this.ai.getNextCard(); const aiCard = this.ai.getNextCard();
if (aiCard && this.opponentLanes.length < MAX_LANES) { if (aiCard && this.opponentLanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard); this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`); this._log(`Opponent deploys ${aiCard.name}`);
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard }); this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
@ -560,6 +590,7 @@ export class CombatEngine {
const fortifyFires = []; const fortifyFires = [];
const strikeFires = []; const strikeFires = [];
const woundFires = []; const woundFires = [];
const venomFires = [];
const liveAllies = allies.filter(c => c.currentHP > 0); const liveAllies = allies.filter(c => c.currentHP > 0);
const liveEnemies = enemies.filter(c => c.currentHP > 0); const liveEnemies = enemies.filter(c => c.currentHP > 0);
for (const card of cards) { for (const card of cards) {
@ -696,6 +727,13 @@ export class CombatEngine {
woundFires.push({ skill: 'wound', source: card, target: ctx.woundTarget, stacks: ctx.woundStacks }); woundFires.push({ skill: 'wound', source: card, target: ctx.woundTarget, stacks: ctx.woundStacks });
this._log(`${card.name} wound: applied ${ctx.woundStacks} stacks to ${ctx.woundTarget.name}`); this._log(`${card.name} wound: applied ${ctx.woundStacks} stacks to ${ctx.woundTarget.name}`);
} }
if (s.name === 'venom' && ctx.venomAllTargets) {
venomFires.push({ skill: 'venom', source: card, isAll: true, targets: ctx.venomAllTargets });
this._log(`${card.name} venom all: applied stacks to ${ctx.venomAllTargets.length} enemies`);
} else if (s.name === 'venom' && ctx.venomTarget) {
venomFires.push({ skill: 'venom', source: card, target: ctx.venomTarget, stacks: ctx.venomStacks });
this._log(`${card.name} venom: applied ${ctx.venomStacks} stacks to ${ctx.venomTarget.name}`);
}
if (s.name === 'strike' && ctx.strikeAllTargets) { if (s.name === 'strike' && ctx.strikeAllTargets) {
const isPlayerCard = this.playerLanes.includes(card) || card === this.playerCommander; const isPlayerCard = this.playerLanes.includes(card) || card === this.playerCommander;
for (const t of ctx.strikeAllTargets) { for (const t of ctx.strikeAllTargets) {
@ -725,7 +763,7 @@ export class CombatEngine {
} }
} }
} }
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, overchargeFires, fortifyFires, strikeFires, woundFires }; return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, overchargeFires, fortifyFires, strikeFires, woundFires, venomFires };
} }
// Emit the 8-step pre-battle sequence and process preBattle skills. // Emit the 8-step pre-battle sequence and process preBattle skills.
@ -746,8 +784,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: [], overchargeFires: [], fortifyFires: [], woundFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
// 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);
@ -756,8 +794,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: [], overchargeFires: [], fortifyFires: [], woundFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
// 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);

View File

@ -9,7 +9,7 @@ export class SkillProcessor {
strike(skill, attacker, defender, allies, enemies, context) { strike(skill, attacker, defender, allies, enemies, context) {
if (!skill.all && defender?.burrowTurns > 0) return; if (!skill.all && defender?.burrowTurns > 0) return;
if (skill.all) { if (skill.all) {
const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0) : (context?.enemyCommander ? [context.enemyCommander] : []); const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0 && !(e.burrowTurns > 0)) : (context?.enemyCommander ? [context.enemyCommander] : []);
if (targets.length === 0) return; if (targets.length === 0) return;
const allTargets = []; const allTargets = [];
for (const t of targets) { for (const t of targets) {
@ -36,7 +36,7 @@ export class SkillProcessor {
// Hits all enemy cards AND the enemy commander for skill.value damage, ignoring armor. // Hits all enemy cards AND the enemy commander for skill.value damage, ignoring armor.
const targets = []; const targets = [];
for (const e of enemies) { for (const e of enemies) {
if (e.currentHP > 0) targets.push(e); if (e.currentHP > 0 && !(e.burrowTurns > 0)) targets.push(e);
} }
if (context?.enemyCommander?.currentHP > 0) targets.push(context.enemyCommander); if (context?.enemyCommander?.currentHP > 0) targets.push(context.enemyCommander);
if (targets.length === 0) return; if (targets.length === 0) return;
@ -51,7 +51,7 @@ export class SkillProcessor {
pierce(skill, attacker, defender, allies, enemies, context) { pierce(skill, attacker, defender, allies, enemies, context) {
if (skill.all) { if (skill.all) {
const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0) : (context?.enemyCommander ? [context.enemyCommander] : []); const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0 && !(e.burrowTurns > 0)) : (context?.enemyCommander ? [context.enemyCommander] : []);
if (targets.length === 0) return; if (targets.length === 0) return;
const allTargets = []; const allTargets = [];
for (const t of targets) { for (const t of targets) {
@ -64,6 +64,7 @@ export class SkillProcessor {
} }
const target = defender || context?.enemyCommander; const target = defender || context?.enemyCommander;
if (!target) return; if (!target) return;
if (target.burrowTurns > 0) return;
// Reduce armor directly (floored at 0); track actual amount so it can be restored. // Reduce armor directly (floored at 0); track actual amount so it can be restored.
const amount = Math.min(skill.value, Math.max(0, target.currentArmor)); const amount = Math.min(skill.value, Math.max(0, target.currentArmor));
target.currentArmor -= amount; target.currentArmor -= amount;
@ -236,7 +237,7 @@ export class SkillProcessor {
const sIdx = idx + offset; const sIdx = idx + offset;
if (sIdx < 0 || sIdx >= enemyLaneCards.length) continue; if (sIdx < 0 || sIdx >= enemyLaneCards.length) continue;
const secondary = enemyLaneCards[sIdx]; const secondary = enemyLaneCards[sIdx];
if (!secondary?.currentHP > 0) continue; if (!secondary || secondary.currentHP <= 0 || secondary.burrowTurns > 0) continue;
const reductions = this._applyJamReductions(secondary, secondaryAmount); const reductions = this._applyJamReductions(secondary, secondaryAmount);
if (reductions.length > 0) { if (reductions.length > 0) {
secondary._tempBuffs = secondary._tempBuffs || []; secondary._tempBuffs = secondary._tempBuffs || [];
@ -269,7 +270,7 @@ export class SkillProcessor {
if (skill.all) { if (skill.all) {
const allTargets = []; const allTargets = [];
for (const t of enemyLaneCards) { for (const t of enemyLaneCards) {
if (!t || t.currentHP <= 0) continue; if (!t || t.currentHP <= 0 || t.burrowTurns > 0) continue;
const amount = Math.min(skill.value, t.currentAttack); const amount = Math.min(skill.value, t.currentAttack);
t.currentAttack = Math.max(0, t.currentAttack - amount); t.currentAttack = Math.max(0, t.currentAttack - amount);
t._tempBuffs = t._tempBuffs || []; t._tempBuffs = t._tempBuffs || [];
@ -366,7 +367,7 @@ export class SkillProcessor {
mortar(skill, attacker, defender, allies, enemies, context) { mortar(skill, attacker, defender, allies, enemies, context) {
if (enemies.length === 0) return; if (enemies.length === 0) return;
if (skill.all) { 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; if (targets.length === 0) return;
const allTargets = []; const allTargets = [];
for (const t of targets) { for (const t of targets) {
@ -435,6 +436,7 @@ export class SkillProcessor {
drain(skill, attacker, defender, allies, enemies, context) { drain(skill, attacker, defender, allies, enemies, context) {
const target = defender || context?.enemyCommander; const target = defender || context?.enemyCommander;
if (!target) return; if (!target) return;
if (target.burrowTurns > 0) return;
const primaryDmg = Math.ceil(skill.value * 0.5); const primaryDmg = Math.ceil(skill.value * 0.5);
const secondaryDmg = Math.ceil(skill.value * 0.25); const secondaryDmg = Math.ceil(skill.value * 0.25);
let totalDrained = 0; let totalDrained = 0;
@ -452,7 +454,7 @@ export class SkillProcessor {
const sIdx = idx + offset; const sIdx = idx + offset;
if (sIdx < 0 || sIdx >= enemyLanes.length) continue; if (sIdx < 0 || sIdx >= enemyLanes.length) continue;
const sec = enemyLanes[sIdx]; const sec = enemyLanes[sIdx];
if (!sec || sec.currentHP <= 0) continue; if (!sec || sec.currentHP <= 0 || sec.burrowTurns > 0) continue;
sec.currentHP -= secondaryDmg; sec.currentHP -= secondaryDmg;
totalDrained += secondaryDmg; totalDrained += secondaryDmg;
secondaries.push({ target: sec, damage: secondaryDmg, laneOffset: offset }); secondaries.push({ target: sec, damage: secondaryDmg, laneOffset: offset });
@ -529,8 +531,9 @@ export class SkillProcessor {
const allTargets = []; const allTargets = [];
for (const t of targets) { for (const t of targets) {
if (t.venomStacks < skill.value) { if (t.venomStacks < skill.value) {
const prevStacks = t.venomStacks;
t.venomStacks = skill.value; t.venomStacks = skill.value;
allTargets.push({ target: t, stacks: skill.value }); allTargets.push({ target: t, stacks: skill.value, prevStacks });
} }
} }
if (context && allTargets.length > 0) context.venomAllTargets = allTargets; if (context && allTargets.length > 0) context.venomAllTargets = allTargets;
@ -618,6 +621,7 @@ export class SkillProcessor {
hack(skill, attacker, defender, allies, enemies, context) { hack(skill, attacker, defender, allies, enemies, context) {
if (!defender || defender.currentHP <= 0) return; if (!defender || defender.currentHP <= 0) return;
if (defender.burrowTurns > 0) return;
const copiableSkills = defender.skills.filter(s => const copiableSkills = defender.skills.filter(s =>
(s.trigger === 'preAttack' || s.trigger === 'preBattle' || s.trigger === 'on_attack') && (s.trigger === 'preAttack' || s.trigger === 'preBattle' || s.trigger === 'on_attack') &&
s.name !== 'hack' s.name !== 'hack'

View File

@ -828,7 +828,7 @@ export class BattleScene extends Phaser.Scene {
// 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 = {};
for (const e of commitEvents) { for (const e of commitEvents) {
if (e.type === 'hive_link') this._preBattleHiveLinks[e.source.instanceId] = e; if (e.type === 'hive_link') this._preBattleHiveLinks[e.deadCardId ?? e.source.instanceId] = e;
} }
// Temporarily restore all preBattle stat changes so _renderState() builds // Temporarily restore all preBattle stat changes so _renderState() builds
@ -843,6 +843,7 @@ export class BattleScene extends Phaser.Scene {
this._restoreOverchargeForDisplay(preBattleEvents); this._restoreOverchargeForDisplay(preBattleEvents);
this._restoreFortifyForDisplay(preBattleEvents); this._restoreFortifyForDisplay(preBattleEvents);
this._restorePreBattleWoundFires(preBattleEvents); this._restorePreBattleWoundFires(preBattleEvents);
this._restorePreBattleVenomFires(preBattleEvents);
this._restoreSiegeFiresForDisplay(preBattleEvents); this._restoreSiegeFiresForDisplay(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
this._renderState(); this._renderState();
@ -855,6 +856,7 @@ export class BattleScene extends Phaser.Scene {
this._reapplyOvercharge(preBattleEvents); this._reapplyOvercharge(preBattleEvents);
this._reapplyFortify(preBattleEvents); this._reapplyFortify(preBattleEvents);
this._reapplyPreBattleWoundFires(preBattleEvents); this._reapplyPreBattleWoundFires(preBattleEvents);
this._reapplyPreBattleVenomFires(preBattleEvents);
this._reapplySiegeFires(preBattleEvents); this._reapplySiegeFires(preBattleEvents);
const newPlayerCard = chosenCard const newPlayerCard = chosenCard
@ -946,12 +948,12 @@ export class BattleScene extends Phaser.Scene {
this._pendingHiveLinks = {}; this._pendingHiveLinks = {};
for (const round of attackRounds) { for (const round of attackRounds) {
for (const e of round.onAttackAllEvents) { for (const e of round.onAttackAllEvents) {
if (e.type === 'hive_link') this._pendingHiveLinks[e.source.instanceId] = e; if (e.type === 'hive_link') this._pendingHiveLinks[e.deadCardId ?? e.source.instanceId] = e;
} }
} }
// Also scan events not yet bucketed into rounds (e.g. from counter kills) // Also scan events not yet bucketed into rounds (e.g. from counter kills)
for (const e of events) { for (const e of events) {
if (e.type === 'hive_link') this._pendingHiveLinks[e.source.instanceId] = e; if (e.type === 'hive_link') this._pendingHiveLinks[e.deadCardId ?? e.source.instanceId] = e;
} }
if (attackRounds.length === 0) { if (attackRounds.length === 0) {
@ -2767,8 +2769,10 @@ export class BattleScene extends Phaser.Scene {
const hasFortify = event.fortifyFires?.length > 0; const hasFortify = event.fortifyFires?.length > 0;
const hasStrike = event.strikeFires?.length > 0; const hasStrike = event.strikeFires?.length > 0;
const hasWound = event.woundFires?.length > 0; const hasWound = event.woundFires?.length > 0;
const hasVenom = event.venomFires?.length > 0;
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasOvercharge || hasFortify || hasStrike || hasWound) { if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasOvercharge || hasFortify || hasStrike || hasWound || hasVenom) {
this._processPreBattleVenomFires(event.venomFires || [], () => {
this._processPreBattleWoundFires(event.woundFires || [], () => { this._processPreBattleWoundFires(event.woundFires || [], () => {
this._processDrainFires(event.drainFires || [], () => { this._processDrainFires(event.drainFires || [], () => {
this._processOverchargeFires(event.overchargeFires || [], () => { this._processOverchargeFires(event.overchargeFires || [], () => {
@ -2792,11 +2796,51 @@ export class BattleScene extends Phaser.Scene {
}); });
}); });
}); });
});
} else { } else {
onComplete(); onComplete();
} }
} }
_processPreBattleVenomFires(fires, onComplete) {
if (!fires || fires.length === 0) { onComplete(); return; }
const next = (idx) => {
if (idx >= fires.length) { onComplete(); return; }
const fire = fires[idx];
const adapted = { ...fire, attacker: fire.source };
if (fire.isAll) {
this._animateVenomApplyAll(adapted, () => next(idx + 1));
} else {
this._animateVenomApply(adapted, () => next(idx + 1));
}
};
next(0);
}
_restorePreBattleVenomFires(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.venomFires || []) {
if (fire.isAll) {
for (const t of fire.targets) t.target.venomStacks = t.prevStacks;
} else if (fire.target) {
fire.target.venomStacks = fire.prevStacks ?? 0;
}
}
}
}
_reapplyPreBattleVenomFires(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.venomFires || []) {
if (fire.isAll) {
for (const t of fire.targets) t.target.venomStacks = t.stacks;
} else if (fire.target) {
fire.target.venomStacks = fire.stacks;
}
}
}
}
_processPreBattleWoundFires(fires, onComplete) { _processPreBattleWoundFires(fires, onComplete) {
if (!fires || fires.length === 0) { onComplete(); return; } if (!fires || fires.length === 0) { onComplete(); return; }
const next = (idx) => { const next = (idx) => {