feat: implement smite skill, rework enfeeble to hive_link/wound, update card skills and flavor texts

- Add 'smite' damage-over-time skill with single/all-target support and animation
- Rename 'enfeeble' to 'hive_link' on xeno cards; add persistent ATK gain on death
- Replace some enfeeble instances with new 'wound' skill (applied on_attack)
- Adjust cleanse trigger from preBattle to postBattle, reduce values
- Rebalance overcharge/fortify/heal/smite skill distributions across factions
- Update all card flavor texts for thematic consistency and personality
- Fix wound badge positioning and origin in CardObject display
- Speed up strike/smite animations (2000→500ms, 500→250ms, etc.)
- Add smiteFires to pre-battle event flow with restore/reapply logic
This commit is contained in:
Brian Fertig 2026-04-04 09:26:01 -06:00
parent d98070915f
commit 0a76d7352b
7 changed files with 194 additions and 87 deletions

Binary file not shown.

View File

@ -165,7 +165,7 @@
]
}
],
"flavorText": "The Centurion's armor has never been breached. Neither has his line.",
"flavorText": "Hold the line for the Emperor! I will defend thee!",
"artKey": "imp_cmd_2"
},
{
@ -250,7 +250,7 @@
]
}
],
"flavorText": "She does not command retreat. She commands annihilation.",
"flavorText": "I must defend the empire's glory, no matter the cost!",
"artKey": "imp_cmd_3"
},
{
@ -267,10 +267,9 @@
"delay": 0,
"skills": [
{
"name": "enfeeble",
"name": "hive_link",
"value": 1,
"all": true,
"trigger": "preBattle"
"all": true
}
]
},
@ -281,10 +280,9 @@
"delay": 0,
"skills": [
{
"name": "enfeeble",
"name": "hive_link",
"value": 2,
"all": true,
"trigger": "preBattle"
"all": true
}
]
},
@ -301,10 +299,9 @@
"trigger": "postBattle"
},
{
"name": "enfeeble",
"name": "hive_link",
"value": 3,
"all": true,
"trigger": "preBattle"
"all": true
}
]
},
@ -316,15 +313,14 @@
"skills": [
{
"name": "cleanse",
"value": 9,
"value": 8,
"all": true,
"trigger": "postBattle"
},
{
"name": "enfeeble",
"name": "hive_link",
"value": 5,
"all": true,
"trigger": "preBattle"
"all": true
}
]
},
@ -336,25 +332,24 @@
"skills": [
{
"name": "cleanse",
"value": 13,
"value": 12,
"all": true,
"trigger": "postBattle"
},
{
"name": "enfeeble",
"name": "hive_link",
"value": 7,
"all": true,
"trigger": "preBattle"
"all": true
},
{
"name": "counter",
"value": 10,
"value": 8,
"trigger": "on_defend"
}
]
}
],
"flavorText": "He built an empire by breaking every enemy from within.",
"flavorText": "It is not I whom has lost their mind, it is YOU!",
"artKey": "imp_cmd_4"
},
{
@ -2131,7 +2126,7 @@
]
}
],
"flavorText": "The Khan takes what he wants. Everything else burns.",
"flavorText": "You don't think I see your weakness commander?",
"artKey": "raider_cmd_1"
},
{
@ -2227,7 +2222,7 @@
]
}
],
"flavorText": "Strip them of their edge. Then make them feel it.",
"flavorText": "Smash it! Smash it all!!!",
"artKey": "raider_cmd_2"
},
{
@ -2244,10 +2239,10 @@
"delay": 0,
"skills": [
{
"name": "enfeeble",
"value": 1,
"name": "wound",
"value": 2,
"all": true,
"trigger": "preBattle"
"trigger": "on_attack"
}
]
},
@ -2258,10 +2253,10 @@
"delay": 0,
"skills": [
{
"name": "enfeeble",
"value": 5,
"name": "wound",
"value": 4,
"all": true,
"trigger": "preBattle"
"trigger": "on_attack"
}
]
},
@ -2279,7 +2274,8 @@
},
{
"name": "wound",
"value": 6,
"value": 5,
"all": true,
"trigger": "on_attack"
}
]
@ -2298,7 +2294,8 @@
},
{
"name": "wound",
"value": 9,
"value": 7,
"all": true,
"trigger": "on_attack"
}
]
@ -2317,7 +2314,8 @@
},
{
"name": "wound",
"value": 13,
"value": 9,
"all": true,
"trigger": "on_attack"
}
]
@ -4070,7 +4068,7 @@
]
}
],
"flavorText": "She drinks deeply, and the battlefield withers.",
"flavorText": "Drain their blood and restore my people.",
"artKey": "bt_cmd_1"
},
{
@ -4164,7 +4162,7 @@
]
}
],
"flavorText": "It does not rule with fear. It rules with hunger.",
"flavorText": "I care little for your pathetic crusade.",
"artKey": "bt_cmd_2"
},
{
@ -4263,7 +4261,7 @@
]
}
],
"flavorText": "Every wound is a gift. Every scar, a sermon.",
"flavorText": "Hurt them.. Then finish them off!",
"artKey": "bt_cmd_3"
},
{
@ -6146,7 +6144,7 @@
]
}
],
"flavorText": "The Alpha does not lead. It commands through shared fury.",
"flavorText": "You shall not see the Alpha or his army coming!",
"artKey": "xeno_cmd_2"
},
{
@ -6261,7 +6259,7 @@
]
}
],
"flavorText": "One mind. A billion claws. It has already seen what you will do next.",
"flavorText": "One mind -- A billion spawn. It has already seen what you will do next.",
"artKey": "xeno_cmd_3"
},
{
@ -7903,6 +7901,12 @@
"name": "cleanse",
"value": 3,
"trigger": "postBattle"
},
{
"name": "rally",
"value": 6,
"all": true,
"trigger": "preBattle"
}
]
},
@ -7916,6 +7920,12 @@
"name": "cleanse",
"value": 5,
"trigger": "postBattle"
},
{
"name": "rally",
"value": 8,
"all": true,
"trigger": "preBattle"
}
]
},
@ -7929,11 +7939,17 @@
"name": "cleanse",
"value": 7,
"trigger": "postBattle"
},
{
"name": "rally",
"value": 10,
"all": true,
"trigger": "preBattle"
}
]
}
],
"flavorText": "All impurity shall be scoured from this sector.",
"flavorText": "10 IF infected = 0 END, 20 infected = 1, 30 GOTO 10",
"artKey": "right_cmd_1"
},
{
@ -7950,8 +7966,9 @@
"delay": 0,
"skills": [
{
"name": "overcharge",
"name": "fortify",
"value": 2,
"all": true,
"trigger": "preBattle"
}
]
@ -7963,8 +7980,9 @@
"delay": 0,
"skills": [
{
"name": "overcharge",
"name": "fortify",
"value": 3,
"all": true,
"trigger": "preBattle"
}
]
@ -7976,8 +7994,15 @@
"delay": 0,
"skills": [
{
"name": "overcharge",
"name": "fortify",
"value": 5,
"all": true,
"trigger": "preBattle"
},
{
"name": "heal",
"value": 2,
"all": true,
"trigger": "preBattle"
}
]
@ -7989,8 +8014,15 @@
"delay": 0,
"skills": [
{
"name": "overcharge",
"name": "fortify",
"value": 7,
"all": true,
"trigger": "preBattle"
},
{
"name": "heal",
"value": 4,
"all": true,
"trigger": "preBattle"
}
]
@ -8002,14 +8034,21 @@
"delay": 0,
"skills": [
{
"name": "overcharge",
"name": "fortify",
"value": 10,
"all": true,
"trigger": "preBattle"
},
{
"name": "heal",
"value": 5,
"all": true,
"trigger": "preBattle"
}
]
}
],
"flavorText": "Pain is the price of devotion. The faithful pay gladly.",
"flavorText": "while (armor < value) do, armor := armour + 1",
"artKey": "right_cmd_2"
},
{
@ -8026,9 +8065,8 @@
"delay": 0,
"skills": [
{
"name": "fortify",
"name": "overcharge",
"value": 2,
"all": true,
"trigger": "preBattle"
}
]
@ -8040,9 +8078,8 @@
"delay": 0,
"skills": [
{
"name": "fortify",
"name": "overcharge",
"value": 3,
"all": true,
"trigger": "preBattle"
}
]
@ -8054,7 +8091,12 @@
"delay": 0,
"skills": [
{
"name": "fortify",
"name": "overcharge",
"value": 4,
"trigger": "preBattle"
},
{
"name": "smite",
"value": 5,
"all": true,
"trigger": "preBattle"
@ -8068,8 +8110,13 @@
"delay": 0,
"skills": [
{
"name": "fortify",
"value": 7,
"name": "overcharge",
"value": 6,
"trigger": "preBattle"
},
{
"name": "smite",
"value": 8,
"all": true,
"trigger": "preBattle"
}
@ -8082,7 +8129,12 @@
"delay": 0,
"skills": [
{
"name": "fortify",
"name": "overcharge",
"value": 8,
"trigger": "preBattle"
},
{
"name": "smite",
"value": 10,
"all": true,
"trigger": "preBattle"
@ -8090,7 +8142,7 @@
]
}
],
"flavorText": "The future is calculated. Your defeat is certain.",
"flavorText": "if holyVirus === NULL { pushHolyVirus(); }",
"artKey": "right_cmd_3"
},
{

View File

@ -206,13 +206,14 @@
{
"name": "hive_link",
"description": "On death, all allied xeno cards gain +value ATK permanently.",
"allDescription": "When any allied card is destroyed, all allied cards gain +value ATK permanently.",
"trigger": "on_death",
"category": "offense"
},
{
"name": "cleanse",
"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.",
"description": "After battle: cleanse value damage-over-time effects (venom, smite, wound) from self.",
"allDescription": "After battle: cleanse value damage-over-time effects from ALL allied cards.",
"trigger": "preBattle",
"category": "support",
"supportsAll": true

View File

@ -591,6 +591,7 @@ export class CombatEngine {
const strikeFires = [];
const woundFires = [];
const venomFires = [];
const smiteFires = [];
const liveAllies = allies.filter(c => c.currentHP > 0);
const liveEnemies = enemies.filter(c => c.currentHP > 0);
for (const card of cards) {
@ -734,6 +735,13 @@ export class CombatEngine {
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 === 'smite' && ctx.smiteAllTargets) {
smiteFires.push({ skill: 'smite', source: card, isAll: true, targets: ctx.smiteAllTargets });
this._log(`${card.name} smite all: applied stacks to ${ctx.smiteAllTargets.length} enemies`);
} else if (s.name === 'smite' && ctx.smiteTarget) {
smiteFires.push({ skill: 'smite', source: card, target: ctx.smiteTarget, stacks: ctx.smiteStacks });
this._log(`${card.name} smite: applied ${ctx.smiteStacks} stacks to ${ctx.smiteTarget.name}`);
}
if (s.name === 'strike' && ctx.strikeAllTargets) {
const isPlayerCard = this.playerLanes.includes(card) || card === this.playerCommander;
for (const t of ctx.strikeAllTargets) {
@ -763,7 +771,7 @@ export class CombatEngine {
}
}
}
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, overchargeFires, fortifyFires, strikeFires, woundFires, venomFires };
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, weakenFires, drainFires, healFires, overchargeFires, fortifyFires, strikeFires, woundFires, venomFires, smiteFires };
}
// Emit the 8-step pre-battle sequence and process preBattle skills.
@ -784,8 +792,8 @@ export class CombatEngine {
const otherAllies = [otherCmd, ...otherLanes];
// Steps 1–2: 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: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [], smiteFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [], protectFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [], smiteFires: [] });
// Steps 3–4: commander offensive buffs (pass actual enemy lanes so all-targeting skills work)
const firstCmdFires = this._collectPreBattleFires([firstCmd], firstAllies, otherAllies, otherCmd, otherLanes);
@ -794,8 +802,8 @@ export class CombatEngine {
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, ...otherCmdFires });
// Steps 5–6: 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: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [], smiteFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [], healFires: [], overchargeFires: [], fortifyFires: [], woundFires: [], venomFires: [], smiteFires: [] });
// Steps 7–8: lane offensive buffs + all skill fires
const firstLaneFires = this._collectPreBattleFires(firstLanes, firstAllies, otherAllies, otherCmd, otherLanes);
@ -1167,6 +1175,7 @@ export class CombatEngine {
const cleanseFires = [];
const liveCards = allSideCards.filter(c => c && c.currentHP > 0);
for (const card of liveCards) {
if ((card.currentDelay || 0) > 0) continue;
for (const s of card.skills) {
if (s.name === 'cleanse' && s.trigger === 'postBattle') {
const ctx = { rng: this.rng };

View File

@ -579,8 +579,9 @@ export class SkillProcessor {
const allTargets = [];
for (const t of targets) {
if (t.smiteStacks < skill.value) {
const prevStacks = t.smiteStacks;
t.smiteStacks = skill.value;
allTargets.push({ target: t, stacks: skill.value });
allTargets.push({ target: t, stacks: skill.value, prevStacks });
}
}
if (context && allTargets.length > 0) context.smiteAllTargets = allTargets;

View File

@ -154,10 +154,10 @@ export class CardObject extends Phaser.GameObjects.Container {
this.add(this.burrowBadge);
this._updateBurrowBadge();
// Wound stack indicator (below burrow badge, red)
// Wound stack indicator (upper-left of image area, below level badge, red)
this.woundBadge = this.scene.add.text(
w / 2 - Math.round(4 * scale),
-h / 2 + bannerH + Math.round(45 * scale),
-w / 2 + Math.round(4 * scale),
-h / 2 + bannerH + Math.round(15 * scale),
'',
{
fontSize: fs(7), color: '#ff4444', fontStyle: 'bold',
@ -165,7 +165,7 @@ export class CardObject extends Phaser.GameObjects.Container {
padding: { x: Math.round(2 * scale), y: Math.round(1 * scale) },
fontFamily: 'Audiowide'
}
).setOrigin(1, 0);
).setOrigin(0, 0);
this.woundBadge.setVisible(false);
this.add(this.woundBadge);
this._updateWoundBadge();

View File

@ -844,6 +844,7 @@ export class BattleScene extends Phaser.Scene {
this._restoreFortifyForDisplay(preBattleEvents);
this._restorePreBattleWoundFires(preBattleEvents);
this._restorePreBattleVenomFires(preBattleEvents);
this._restorePreBattleSmiteFires(preBattleEvents);
this._restoreSiegeFiresForDisplay(preBattleEvents);
// Render the field with all newly deployed cards but pre-combat stats
this._renderState();
@ -857,6 +858,7 @@ export class BattleScene extends Phaser.Scene {
this._reapplyFortify(preBattleEvents);
this._reapplyPreBattleWoundFires(preBattleEvents);
this._reapplyPreBattleVenomFires(preBattleEvents);
this._reapplyPreBattleSmiteFires(preBattleEvents);
this._reapplySiegeFires(preBattleEvents);
const newPlayerCard = chosenCard
@ -1074,7 +1076,7 @@ export class BattleScene extends Phaser.Scene {
targets: knife,
x: targetObj.x,
y: targetObj.y,
duration: 2000,
duration: 500,
ease: 'Quad.In',
onComplete: () => {
if (knife.scene) knife.destroy();
@ -1143,7 +1145,7 @@ export class BattleScene extends Phaser.Scene {
scaleX: { from: effectSprite.scaleX * 0.8, to: effectSprite.scaleX * 1.5 },
scaleY: { from: effectSprite.scaleY * 0.8, to: effectSprite.scaleY * 1.5 },
alpha: { from: 0.9, to: 0.2 },
duration: 500,
duration: 250,
yoyo: true,
repeat: 1,
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }
@ -2770,21 +2772,25 @@ export class BattleScene extends Phaser.Scene {
const hasStrike = event.strikeFires?.length > 0;
const hasWound = event.woundFires?.length > 0;
const hasVenom = event.venomFires?.length > 0;
const hasSmite = event.smiteFires?.length > 0;
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 || [], () => {
this._processBuffAnimations(event.buffs || [], () => {
this._processFortifyFires(event.fortifyFires || [], () => {
this._processSiegeFires(event.siegeFires || [], () => {
this._processPreBattleStrikeFires(event.strikeFires || [], () => {
this._processProtectFires(event.protectFires || [], () => {
this._processEnfeebeFires(event.enfeebeFires || [], () => {
this._processJamFires(event.jamFires || [], () => {
this._processWeakenFires(event.weakenFires || [], () => {
this._processHealFires(event.healFires || [], onComplete);
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasWeaken || hasDrain || hasHeal || hasOvercharge || hasFortify || hasStrike || hasWound || hasVenom || hasSmite) {
this._processPreBattleSmiteFires(event.smiteFires || [], () => {
this._processPreBattleVenomFires(event.venomFires || [], () => {
this._processPreBattleWoundFires(event.woundFires || [], () => {
this._processDrainFires(event.drainFires || [], () => {
this._processOverchargeFires(event.overchargeFires || [], () => {
this._processBuffAnimations(event.buffs || [], () => {
this._processFortifyFires(event.fortifyFires || [], () => {
this._processSiegeFires(event.siegeFires || [], () => {
this._processPreBattleStrikeFires(event.strikeFires || [], () => {
this._processProtectFires(event.protectFires || [], () => {
this._processEnfeebeFires(event.enfeebeFires || [], () => {
this._processJamFires(event.jamFires || [], () => {
this._processWeakenFires(event.weakenFires || [], () => {
this._processHealFires(event.healFires || [], onComplete);
});
});
});
});
});
@ -2794,14 +2800,52 @@ export class BattleScene extends Phaser.Scene {
});
});
});
});
});
});
});
} else {
onComplete();
}
}
_processPreBattleSmiteFires(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._animateSmiteApplyAll(adapted, () => next(idx + 1));
} else {
this._animateSmiteApply(adapted, () => next(idx + 1));
}
};
next(0);
}
_restorePreBattleSmiteFires(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.smiteFires || []) {
if (fire.isAll) {
for (const t of fire.targets) t.target.smiteStacks = t.prevStacks;
} else if (fire.target) {
fire.target.smiteStacks = fire.prevStacks ?? 0;
}
}
}
}
_reapplyPreBattleSmiteFires(preBattleEvents) {
for (const event of preBattleEvents) {
for (const fire of event.smiteFires || []) {
if (fire.isAll) {
for (const t of fire.targets) t.target.smiteStacks = t.stacks;
} else if (fire.target) {
fire.target.smiteStacks = fire.stacks;
}
}
}
}
_processPreBattleVenomFires(fires, onComplete) {
if (!fires || fires.length === 0) { onComplete(); return; }
const next = (idx) => {
@ -3912,7 +3956,7 @@ export class BattleScene extends Phaser.Scene {
targets: enfeebleSprite,
scaleX: BASE_SCALE * 1.25,
scaleY: BASE_SCALE * 1.25,
duration: 200,
duration: 100,
ease: 'Sine.easeInOut',
yoyo: true,
repeat: 2,
@ -3969,7 +4013,7 @@ export class BattleScene extends Phaser.Scene {
this.tweens.add({
targets: enfeebleSprite,
scaleX: BASE_SCALE * 1.25, scaleY: BASE_SCALE * 1.25,
duration: 200, ease: 'Sine.easeInOut', yoyo: true, repeat: 2,
duration: 100, ease: 'Sine.easeInOut', yoyo: true, repeat: 2,
onComplete: () => {
if (!enfeebleSprite.scene) { onComplete(); return; }
this.tweens.add({
@ -4855,7 +4899,7 @@ export class BattleScene extends Phaser.Scene {
this.tweens.add({
targets: projectile,
x: targetObj.x, y: targetObj.y,
duration: 2000,
duration: 1750,
ease: 'Quad.In',
onComplete: () => {
if (projectile.scene) projectile.destroy();
@ -4913,7 +4957,7 @@ export class BattleScene extends Phaser.Scene {
targets: effectSprite,
scaleX: { from: 0.8, to: 1.5 },
scaleY: { from: 0.8, to: 1.5 },
duration: 250,
duration: 150,
yoyo: true,
repeat: 1,
onComplete: () => { if (effectSprite.scene) effectSprite.destroy(); }