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

View File

@ -44,15 +44,23 @@ export class CombatEngine {
}
// 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() {
if (this.playerHand.length > 0 && this.playerLanes.length < MAX_LANES) {
const card = this.playerHand.shift();
this._applyCommanderBurrowBonus(card, this.playerCommander);
this.playerLanes.push(card);
this._log(`Player deploys ${card.name}`);
this.events.push({ type: 'deploy', side: 'player', card });
}
const aiCard = this.ai.getNextCard();
if (aiCard && this.opponentLanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`);
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
@ -203,6 +211,7 @@ export class CombatEngine {
this.skillProcessor.process(s, commander, null,
[commander, ...lanes], [enemyCommander, ...enemyLanes], ctx);
if (ctx.spawnedDrone && lanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(ctx.spawnedDrone, commander);
lanes.push(ctx.spawnedDrone);
const side = (commander === this.playerCommander) ? 'player' : 'opponent';
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 });
}
}
// 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
@ -464,6 +490,7 @@ export class CombatEngine {
// and should not be ticked the same turn it enters the field (same rule as
// player cards, which deploy after _activationPhase() runs in beginCommit).
aiCard._justDeployed = true;
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`);
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);
if (idx !== -1) {
this.playerHand.splice(idx, 1);
this._applyCommanderBurrowBonus(chosenCard, this.playerCommander);
this.playerLanes.push(chosenCard);
this._log(`Player deploys ${chosenCard.name}`);
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);
if (idx !== -1) {
this.playerHand.splice(idx, 1);
this._applyCommanderBurrowBonus(chosenCard, this.playerCommander);
this.playerLanes.push(chosenCard);
this._log(`Player deploys ${chosenCard.name}`);
this.events.push({ type: 'deploy', side: 'player', card: chosenCard });
@ -526,6 +555,7 @@ export class CombatEngine {
if (this.playerGoesFirst) {
const aiCard = this.ai.getNextCard();
if (aiCard && this.opponentLanes.length < MAX_LANES) {
this._applyCommanderBurrowBonus(aiCard, this.opponentCommander);
this.opponentLanes.push(aiCard);
this._log(`Opponent deploys ${aiCard.name}`);
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
@ -560,6 +590,7 @@ export class CombatEngine {
const fortifyFires = [];
const strikeFires = [];
const woundFires = [];
const venomFires = [];
const liveAllies = allies.filter(c => c.currentHP > 0);
const liveEnemies = enemies.filter(c => c.currentHP > 0);
for (const card of cards) {
@ -696,6 +727,13 @@ export class CombatEngine {
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}`);
}
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) {
const isPlayerCard = this.playerLanes.includes(card) || card === this.playerCommander;
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.
@ -746,8 +784,8 @@ export class CombatEngine {
const otherAllies = [otherCmd, ...otherLanes];
// 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: otherSide, card: otherCmd, 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: [], venomFires: [] });
// Steps 34: commander offensive buffs (pass actual enemy lanes so all-targeting skills work)
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 });
// 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: otherSide, cards: otherLanes, 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: [], venomFires: [] });
// Steps 78: lane offensive buffs + all skill fires
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) {
if (!skill.all && defender?.burrowTurns > 0) return;
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;
const allTargets = [];
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.
const targets = [];
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 (targets.length === 0) return;
@ -51,7 +51,7 @@ export class SkillProcessor {
pierce(skill, attacker, defender, allies, enemies, context) {
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;
const allTargets = [];
for (const t of targets) {
@ -64,6 +64,7 @@ export class SkillProcessor {
}
const target = defender || context?.enemyCommander;
if (!target) return;
if (target.burrowTurns > 0) return;
// 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));
target.currentArmor -= amount;
@ -236,7 +237,7 @@ export class SkillProcessor {
const sIdx = idx + offset;
if (sIdx < 0 || sIdx >= enemyLaneCards.length) continue;
const secondary = enemyLaneCards[sIdx];
if (!secondary?.currentHP > 0) continue;
if (!secondary || secondary.currentHP <= 0 || secondary.burrowTurns > 0) continue;
const reductions = this._applyJamReductions(secondary, secondaryAmount);
if (reductions.length > 0) {
secondary._tempBuffs = secondary._tempBuffs || [];
@ -269,7 +270,7 @@ export class SkillProcessor {
if (skill.all) {
const allTargets = [];
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);
t.currentAttack = Math.max(0, t.currentAttack - amount);
t._tempBuffs = t._tempBuffs || [];
@ -366,7 +367,7 @@ export class SkillProcessor {
mortar(skill, attacker, defender, allies, enemies, context) {
if (enemies.length === 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) {
@ -435,6 +436,7 @@ export class SkillProcessor {
drain(skill, attacker, defender, allies, enemies, context) {
const target = defender || context?.enemyCommander;
if (!target) return;
if (target.burrowTurns > 0) return;
const primaryDmg = Math.ceil(skill.value * 0.5);
const secondaryDmg = Math.ceil(skill.value * 0.25);
let totalDrained = 0;
@ -452,7 +454,7 @@ export class SkillProcessor {
const sIdx = idx + offset;
if (sIdx < 0 || sIdx >= enemyLanes.length) continue;
const sec = enemyLanes[sIdx];
if (!sec || sec.currentHP <= 0) continue;
if (!sec || sec.currentHP <= 0 || sec.burrowTurns > 0) continue;
sec.currentHP -= secondaryDmg;
totalDrained += secondaryDmg;
secondaries.push({ target: sec, damage: secondaryDmg, laneOffset: offset });
@ -529,8 +531,9 @@ export class SkillProcessor {
const allTargets = [];
for (const t of targets) {
if (t.venomStacks < skill.value) {
const prevStacks = t.venomStacks;
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;
@ -618,6 +621,7 @@ export class SkillProcessor {
hack(skill, attacker, defender, allies, enemies, context) {
if (!defender || defender.currentHP <= 0) return;
if (defender.burrowTurns > 0) return;
const copiableSkills = defender.skills.filter(s =>
(s.trigger === 'preAttack' || s.trigger === 'preBattle' || s.trigger === 'on_attack') &&
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
this._preBattleHiveLinks = {};
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
@ -843,6 +843,7 @@ export class BattleScene extends Phaser.Scene {
this._restoreOverchargeForDisplay(preBattleEvents);
this._restoreFortifyForDisplay(preBattleEvents);
this._restorePreBattleWoundFires(preBattleEvents);
this._restorePreBattleVenomFires(preBattleEvents);
this._restoreSiegeFiresForDisplay(preBattleEvents);
// Render the field with all newly deployed cards but pre-combat stats
this._renderState();
@ -855,6 +856,7 @@ export class BattleScene extends Phaser.Scene {
this._reapplyOvercharge(preBattleEvents);
this._reapplyFortify(preBattleEvents);
this._reapplyPreBattleWoundFires(preBattleEvents);
this._reapplyPreBattleVenomFires(preBattleEvents);
this._reapplySiegeFires(preBattleEvents);
const newPlayerCard = chosenCard
@ -946,12 +948,12 @@ export class BattleScene extends Phaser.Scene {
this._pendingHiveLinks = {};
for (const round of attackRounds) {
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)
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) {
@ -2767,8 +2769,10 @@ export class BattleScene extends Phaser.Scene {
const hasFortify = event.fortifyFires?.length > 0;
const hasStrike = event.strikeFires?.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._processDrainFires(event.drainFires || [], () => {
this._processOverchargeFires(event.overchargeFires || [], () => {
@ -2792,11 +2796,51 @@ export class BattleScene extends Phaser.Scene {
});
});
});
});
} else {
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) {
if (!fires || fires.length === 0) { onComplete(); return; }
const next = (idx) => {