fix: #346 #345 spell casting guards and equipment spell wiring
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m4s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m4s
Issue #346: Added floorHP > 0 guard to primary and equipment spell casting while loops in combat-actions.ts. Previously spells continued casting and draining mana after all enemies were dead. Issue #345 Bug A: Populated equipmentSpellStates from equipped gear in gameStore.ts combat tick setup using getActiveEquipmentSpells(). Previously equipment spell enchantments (e.g., spell_manaBolt on casters) never fired during combat because equipmentSpellStates was always empty. Issue #345 Bug B: Added deductWeaponEnchantCosts() helper in combat-damage.ts and wired it into the melee loop in combat-actions.ts. Weapon enchant spells (fireBlade, frostBlade, lightningBlade, voidBlade) now properly deduct mana per melee hit instead of providing free elemental bonus damage. All 1141 tests pass (65 test files), no regressions. Added 5 new regression tests. Files changed: - combat-actions.ts: +floorHP>0 guards, weapon enchant cost deduction - combat-damage.ts: +deductWeaponEnchantCosts() helper - gameStore.ts: +equipment spell state population from equipped gear - spell-cast-floorhp-guard.test.ts: new regression test file
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
processGolemManaRegen,
|
||||
} from './golem-combat-actions';
|
||||
import { processGolemAttacksFromStore } from './golem-combat-helpers';
|
||||
import { applyDamageToRoom } from './combat-damage';
|
||||
import { applyDamageToRoom, deductWeaponEnchantCosts } from './combat-damage';
|
||||
|
||||
// ─── Result Type ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -144,7 +144,7 @@ export function processCombatTick(
|
||||
// Process complete casts for active spell
|
||||
let safetyCounter = 0;
|
||||
const MAX_CASTS_PER_TICK = 100;
|
||||
while (weaponCastProgress['primary'] >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements) && safetyCounter < MAX_CASTS_PER_TICK) {
|
||||
while (weaponCastProgress['primary'] >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements) && floorHP > 0 && safetyCounter < MAX_CASTS_PER_TICK) {
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
@@ -208,7 +208,7 @@ export function processCombatTick(
|
||||
|
||||
let eSafetyCounter = 0;
|
||||
const MAX_E_CASTS_PER_TICK = 100;
|
||||
while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements) && eSafetyCounter < MAX_E_CASTS_PER_TICK) {
|
||||
while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements) && floorHP > 0 && eSafetyCounter < MAX_E_CASTS_PER_TICK) {
|
||||
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
||||
rawMana = eAfterCost.rawMana;
|
||||
elements = eAfterCost.elements;
|
||||
@@ -274,6 +274,11 @@ export function processCombatTick(
|
||||
let meleeSafetyCounter = 0;
|
||||
while (meleeProgress >= 1 && meleeSafetyCounter < 100) {
|
||||
const meleeDamage = calcMeleeDamage(swordInstance as EquipmentInstance, swordType, floorElems[0]);
|
||||
|
||||
// Deduct mana cost for weapon enchant spells (fireBlade, frostBlade, etc.)
|
||||
const enchantCost = deductWeaponEnchantCosts(swordInstance as EquipmentInstance, rawMana, elements);
|
||||
rawMana = enchantCost.rawMana;
|
||||
elements = enchantCost.elements;
|
||||
// Get the current target enemy (lowest HP, matching focus-fire targeting in applyDamageToRoom)
|
||||
const currentRoomState = get().currentRoom;
|
||||
const livingEnemies = currentRoomState.enemies.filter(e => e.hp > 0);
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
// ─── Per-Enemy Damage Application (spec §3.2) ─────────────────────────────────
|
||||
// Extracted from combat-actions.ts to stay under the 400-line file limit.
|
||||
|
||||
import { ENCHANTMENT_EFFECTS, ENCHANTMENT_SPELLS } from '../data/enchantment-effects';
|
||||
import { canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import type { CombatStore, CombatState } from './combat-state.types';
|
||||
import type { EnemyState } from '../types';
|
||||
import type { EnemyState, EquipmentInstance } from '../types';
|
||||
|
||||
/**
|
||||
* Deduct mana costs for weapon enchant spells on a sword (fireBlade, frostBlade, etc.).
|
||||
* Called once per melee hit in the combat tick.
|
||||
* Returns updated { rawMana, elements } reflecting any enchant costs.
|
||||
*/
|
||||
export function deductWeaponEnchantCosts(
|
||||
swordInstance: EquipmentInstance,
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
||||
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
|
||||
let rm = rawMana;
|
||||
let el = elements;
|
||||
for (const ench of swordInstance.enchantments) {
|
||||
const enchantEffect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
if (enchantEffect?.effect.type === 'special' && enchantEffect.effect.specialId) {
|
||||
const spellDef = ENCHANTMENT_SPELLS[enchantEffect.effect.specialId];
|
||||
if (spellDef?.cost) {
|
||||
const afterCost = deductSpellCost(spellDef.cost, rm, el);
|
||||
rm = afterCost.rawMana;
|
||||
el = afterCost.elements;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { rawMana: rm, elements: el };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the enemy with the lowest current HP in the room (focus-fire targeting).
|
||||
|
||||
@@ -6,7 +6,7 @@ import { computeEquipmentEffects } from '../effects';
|
||||
import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
||||
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight } from '../utils';
|
||||
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight, getActiveEquipmentSpells } from '../utils';
|
||||
import { getElementDistance } from '../utils/element-distance';
|
||||
import { computeConversionRates } from '../utils/conversion-rates';
|
||||
import { mergePerElementCapBonuses } from '../utils/element-cap-bonus';
|
||||
@@ -277,6 +277,18 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
const inst = ctx.crafting.equipmentInstances?.[iid];
|
||||
if (inst && EQUIPMENT_TYPES[inst.typeId]?.category === 'sword') equippedSwords[iid] = inst;
|
||||
}
|
||||
// Populate equipment spell states from equipped gear
|
||||
const activeEquipSpells = getActiveEquipmentSpells(
|
||||
ctx.crafting.equippedInstances || {},
|
||||
ctx.crafting.equipmentInstances || {},
|
||||
);
|
||||
const equipmentSpellStates = activeEquipSpells.map((s) => ({
|
||||
spellId: s.spellId,
|
||||
sourceEquipment: s.equipmentId,
|
||||
castProgress: 0,
|
||||
}));
|
||||
useCombatStore.setState({ equipmentSpellStates });
|
||||
|
||||
const cr = useCombatStore.getState().processCombatTick(
|
||||
rawMana, elements, maxMana, 1,
|
||||
combatCbs.onFloorCleared,
|
||||
|
||||
Reference in New Issue
Block a user