feat: implement DoT/debuff runtime system (spec §6, AC-12, AC-13)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
- Add ActiveEffect, EffectType types to game.ts; activeEffects + effectiveArmor on EnemyState - Add SpellOnHitEffect + onHitEffect field to SpellDefinition - Wire onHitEffect to fire (burn), death (curse), lightning (armor_corrode), frost (freeze), soul (bypassArmor burn) - Add applyOnHitEffect() — applies on-hit effect on successful spell hit (spec §6.2) - Add processDoTPhase() — ticks all active effects after weapon/golem attacks (spec §6.3) - Add bypassArmor/bypassBarrier support in applyEnemyDefenses() (AC-13) - Export standalone applyEnemyDefenses from combat-tick.ts for DoT pipeline - Split DoT runtime into separate dot-runtime.ts (135 lines) to keep combat-actions.ts under 400 lines - Update all enemy generation sites with activeEffects/effectiveArmor defaults - Fix test helpers for new required fields All 921 tests pass (45 test files)
This commit is contained in:
@@ -37,13 +37,62 @@ interface BuildCombatCallbacksParams {
|
||||
effects: ComputedEffects;
|
||||
maxMana: number;
|
||||
addLog: (msg: string) => void;
|
||||
useCombatStore: { setState: (s: Record<string, unknown>) => void };
|
||||
useCombatStore: { setState: (s: Record<string, unknown>) => void; getState: () => Record<string, unknown> };
|
||||
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } };
|
||||
}
|
||||
|
||||
/** Speed-room bonus added to agile dodge chance (spec §4.5) */
|
||||
const SPEED_ROOM_DODGE_BONUS = 0.20;
|
||||
|
||||
// ─── Standalone Enemy Defenses (for DoT/debuff pipeline) ─────────────────────
|
||||
|
||||
/**
|
||||
* Apply regular enemy defenses: dodge → barrier → armor (spec §5.2).
|
||||
* Returns modified damage, or 0 on dodge.
|
||||
* Exported for use by the DoT/debuff tick processing system (spec §6.3).
|
||||
*
|
||||
* @param bypassArmor — if true, skip armor reduction entirely (spec §6.4, AC-13)
|
||||
* @param bypassBarrier — if true, skip barrier absorption (spec §6.4)
|
||||
*/
|
||||
export function applyEnemyDefenses(
|
||||
dmg: number,
|
||||
enemy: EnemyState | null,
|
||||
roomType: string,
|
||||
addLog: (msg: string) => void,
|
||||
bypassArmor?: boolean,
|
||||
bypassBarrier?: boolean,
|
||||
): number {
|
||||
if (!enemy) return dmg;
|
||||
|
||||
// 1. Dodge check (spec §5.2, §4.5)
|
||||
let effectiveDodge = enemy.dodgeChance;
|
||||
if (roomType === 'speed') {
|
||||
const hasAgile = enemy.name.toLowerCase().includes('agile');
|
||||
if (hasAgile) {
|
||||
effectiveDodge = Math.min(0.75, enemy.dodgeChance + SPEED_ROOM_DODGE_BONUS);
|
||||
}
|
||||
}
|
||||
if (effectiveDodge > 0 && Math.random() < effectiveDodge) {
|
||||
addLog('Attack dodged!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. Barrier absorption (percentage, spec §5.2) — skipped if bypassBarrier
|
||||
if (!bypassBarrier && enemy.barrier && enemy.barrier > 0) {
|
||||
dmg *= (1 - enemy.barrier);
|
||||
}
|
||||
|
||||
// 3. Armor reduction — skipped if bypassArmor (spec §6.4, AC-13)
|
||||
if (!bypassArmor) {
|
||||
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
||||
if (armorValue && armorValue > 0) {
|
||||
dmg *= (1 - armorValue);
|
||||
}
|
||||
}
|
||||
|
||||
return dmg;
|
||||
}
|
||||
|
||||
export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
const { ctx, effects, maxMana, useCombatStore, usePrestigeStore } = params;
|
||||
|
||||
@@ -85,46 +134,8 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
return { ...enemy, barrier: recharged };
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply regular enemy defenses: dodge → barrier → armor (spec §5.2).
|
||||
* Returns modified damage, or 0 on dodge.
|
||||
* This is the single defense pipeline used for ALL enemy hits (not just guardians).
|
||||
*/
|
||||
const applyEnemyDefenses = (
|
||||
dmg: number,
|
||||
enemy: EnemyState | null,
|
||||
roomType: string,
|
||||
addLog: (msg: string) => void,
|
||||
): number => {
|
||||
if (!enemy) return dmg;
|
||||
|
||||
// 1. Dodge check (spec §5.2, §4.5)
|
||||
let effectiveDodge = enemy.dodgeChance;
|
||||
if (roomType === 'speed') {
|
||||
// Agile + speed room: additive dodge bonus, capped at 0.75
|
||||
const hasAgile = enemy.name.toLowerCase().includes('agile');
|
||||
if (hasAgile) {
|
||||
effectiveDodge = Math.min(0.75, enemy.dodgeChance + SPEED_ROOM_DODGE_BONUS);
|
||||
}
|
||||
}
|
||||
if (effectiveDodge > 0 && Math.random() < effectiveDodge) {
|
||||
addLog('Attack dodged!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. Barrier absorption (percentage, spec §5.2)
|
||||
if (enemy.barrier && enemy.barrier > 0) {
|
||||
dmg *= (1 - enemy.barrier);
|
||||
}
|
||||
|
||||
// 3. Armor reduction — use effectiveArmor (after corrode) if available, else base armor (spec §5.2)
|
||||
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
||||
if (armorValue && armorValue > 0) {
|
||||
dmg *= (1 - armorValue);
|
||||
}
|
||||
|
||||
return dmg;
|
||||
};
|
||||
// Local reference to the module-level applyEnemyDefenses
|
||||
const defApply = applyEnemyDefenses;
|
||||
|
||||
/**
|
||||
* Create the onDamageDealt callback for this tick.
|
||||
@@ -150,7 +161,7 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
}
|
||||
|
||||
// Apply regular enemy defenses for ALL enemies (spec §5.2)
|
||||
dmg = applyEnemyDefenses(dmg, defCtx.enemy, defCtx.roomType, addLog);
|
||||
dmg = defApply(dmg, defCtx.enemy, defCtx.roomType, addLog);
|
||||
|
||||
// Guardian-specific defensive pipeline (shield → barrier → health regen, spec §5.3)
|
||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||
|
||||
Reference in New Issue
Block a user