feat: implement sword/melee auto-attack system (spec §3.1, §4.3)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
- Add calcMeleeDamage() with elemental matchup for enchanted swords - Add meleeSwordProgress per-instance accumulator to combat state - Add melee branch in processCombatTick (no mana cost, no Executioner/Berserker) - Add baseDamage/attackSpeed stats to all 5 sword types - Wire equippedSwords through gameStore to combat tick pipeline - 16 new regression tests, all 937 tests pass
This commit is contained in:
@@ -2,14 +2,13 @@
|
||||
// Pure combat logic — no cross-store getState() calls.
|
||||
// All external data (signedPacts, etc.) is passed in as parameters.
|
||||
|
||||
import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
|
||||
import { SPELLS_DEF, HOURS_PER_TICK, EQUIPMENT_TYPES } from '../constants';
|
||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||
import type { CombatStore, CombatState } from './combat-state.types';
|
||||
import type { SpellState, EnemyState } from '../types';
|
||||
import type { SpellState, EnemyState, EquipmentInstance } from '../types';
|
||||
import { applyOnHitEffect, processDoTPhase } from './dot-runtime';
|
||||
import type { ActiveGolem } from '../types';
|
||||
import type { SpellOnHitEffect } from '../types/spells';
|
||||
import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, calcMeleeDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { processGolemMaintenance, processGolemAttacks } from './golem-combat-actions';
|
||||
|
||||
@@ -34,6 +33,7 @@ function makeDefaultCombatTickResult(
|
||||
castProgress: state.castProgress,
|
||||
equipmentSpellStates: state.equipmentSpellStates,
|
||||
activeGolems,
|
||||
meleeSwordProgress: state.meleeSwordProgress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface CombatTickResult {
|
||||
castProgress: number;
|
||||
equipmentSpellStates: CombatState['equipmentSpellStates'];
|
||||
activeGolems: ActiveGolem[];
|
||||
meleeSwordProgress: Record<string, number>;
|
||||
}
|
||||
|
||||
export function processCombatTick(
|
||||
@@ -75,6 +76,7 @@ export function processCombatTick(
|
||||
bypassArmor?: boolean,
|
||||
bypassBarrier?: boolean,
|
||||
) => number,
|
||||
equippedSwords?: Record<string, EquipmentInstance>,
|
||||
): CombatTickResult {
|
||||
const state = get();
|
||||
const logMessages: string[] = [];
|
||||
@@ -84,212 +86,225 @@ export function processCombatTick(
|
||||
return makeDefaultCombatTickResult(rawMana, elements, state, golemancyState.activeGolems);
|
||||
}
|
||||
|
||||
// ─── Golem maintenance (spec §9.5) ──────────────────────────────────────
|
||||
const maintenanceResult = processGolemMaintenance(
|
||||
golemancyState.activeGolems,
|
||||
rawMana,
|
||||
elements,
|
||||
);
|
||||
let activeGolems = maintenanceResult.maintainedGolems;
|
||||
rawMana = maintenanceResult.rawMana;
|
||||
elements = maintenanceResult.elements;
|
||||
logMessages.push(...maintenanceResult.logMessages);
|
||||
|
||||
// Write maintained golems back immediately so tick state stays consistent
|
||||
set({ golemancy: { ...state.golemancy, activeGolems } });
|
||||
|
||||
const spellId = state.activeSpell;
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) {
|
||||
// Even if no spell is configured, golems can still fight
|
||||
// Process golem attacks even without a valid spell
|
||||
if (activeGolems.length > 0) {
|
||||
const golemAttackResult = processGolemAttacks(
|
||||
activeGolems,
|
||||
rawMana,
|
||||
elements,
|
||||
state.floorHP,
|
||||
state.floorMaxHP,
|
||||
state.currentFloor,
|
||||
onDamageDealt,
|
||||
golemApplyDamageToRoom,
|
||||
);
|
||||
rawMana = golemAttackResult.rawMana;
|
||||
elements = golemAttackResult.elements;
|
||||
activeGolems = golemAttackResult.activeGolems;
|
||||
logMessages.push(...golemAttackResult.logMessages);
|
||||
|
||||
// Check if golems cleared the room
|
||||
const newFloorHP = golemApplyDamageToRoom(0);
|
||||
if (newFloorHP.roomCleared || golemAttackResult.totalDamageDealt > 0) {
|
||||
// Re-check floor state after golem attacks
|
||||
const newState = get();
|
||||
if (newState.floorHP <= 0) {
|
||||
return makeDefaultCombatTickResult(rawMana, elements, state, activeGolems);
|
||||
}
|
||||
}
|
||||
}
|
||||
return makeDefaultCombatTickResult(rawMana, elements, state, activeGolems);
|
||||
}
|
||||
|
||||
try {
|
||||
// Compute discipline bonuses once per tick
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
// ─── Golem maintenance (spec §9.5) ──────────────────────────────────────
|
||||
const maintenanceResult = processGolemMaintenance(
|
||||
golemancyState.activeGolems,
|
||||
rawMana,
|
||||
elements,
|
||||
);
|
||||
let activeGolems = maintenanceResult.maintainedGolems;
|
||||
rawMana = maintenanceResult.rawMana;
|
||||
elements = maintenanceResult.elements;
|
||||
logMessages.push(...maintenanceResult.logMessages);
|
||||
|
||||
// Calculate cast speed (no skill bonus)
|
||||
const totalAttackSpeed = attackSpeedMult;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
||||
// Write maintained golems back immediately so tick state stays consistent
|
||||
set({ golemancy: { ...state.golemancy, activeGolems } });
|
||||
|
||||
const spellId = state.activeSpell;
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
|
||||
let castProgress = (state.castProgress || 0) + progressPerTick;
|
||||
let floorHP = state.floorHP;
|
||||
let currentFloor = state.currentFloor;
|
||||
let floorMaxHP = state.floorMaxHP;
|
||||
|
||||
// Process complete casts for active spell (safety counter prevents infinite loop)
|
||||
let safetyCounter = 0;
|
||||
const MAX_CASTS_PER_TICK = 100;
|
||||
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements) && safetyCounter < MAX_CASTS_PER_TICK) {
|
||||
// Deduct spell cost
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
// Calculate base damage (without elemental bonus first)
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
const baseDamage = calcDamage(
|
||||
{ signedPacts },
|
||||
spellId,
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
// Apply elemental bonus — for multi-element guardians, use all elements
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
const floorElems = guardian && guardian.element.length > 0
|
||||
? guardian.element
|
||||
: [floorElement];
|
||||
const multiElemBonus = getMultiElementBonus(spellDef.elem, floorElems);
|
||||
const damage = baseDamage * multiElemBonus;
|
||||
|
||||
// Let gameStore apply damage modifiers (executioner, berserker)
|
||||
const result = onDamageDealt(damage);
|
||||
rawMana = result.rawMana;
|
||||
elements = result.elements;
|
||||
const finalDamage = result.modifiedDamage || damage;
|
||||
|
||||
// Guard against NaN damage — if damage is not finite, stop processing
|
||||
if (!Number.isFinite(finalDamage)) {
|
||||
logMessages.push('⚠️ Combat stopped: invalid damage value');
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - finalDamage);
|
||||
castProgress -= 1;
|
||||
safetyCounter++;
|
||||
|
||||
// Apply on-hit effect (DoT/debuff) to enemy (spec §6.2)
|
||||
applyOnHitEffect(get, set, spellId, logMessages);
|
||||
|
||||
// Check if room/floor is cleared
|
||||
if (floorHP <= 0) {
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!guardian);
|
||||
|
||||
// ── Spec: room-aware advancement (climbing spec §4.4) ──────────────
|
||||
// Instead of directly incrementing the floor, delegate to the store's
|
||||
// advanceRoomOrFloor which handles room-by-room and floor transitions.
|
||||
get().advanceRoomOrFloor();
|
||||
|
||||
// Re-read state after advancement
|
||||
const newState = get();
|
||||
currentFloor = newState.currentFloor;
|
||||
floorMaxHP = newState.floorMaxHP;
|
||||
floorHP = newState.floorHP;
|
||||
castProgress = 0;
|
||||
|
||||
if (guardian) {
|
||||
logMessages.push(`\u2694\ufe0f ${guardian.name} defeated!`);
|
||||
} else if (currentFloor % 5 === 0) {
|
||||
logMessages.push(`\ud83c\udff0 Floor ${currentFloor - 1} cleared!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process equipment spell states (for progress bars in UI)
|
||||
let castProgress = state.castProgress;
|
||||
const updatedEquipmentSpellStates = [...state.equipmentSpellStates];
|
||||
for (let i = 0; i < updatedEquipmentSpellStates.length; i++) {
|
||||
const eSpell = updatedEquipmentSpellStates[i];
|
||||
const eSpellDef = SPELLS_DEF[eSpell.spellId];
|
||||
if (!eSpellDef) continue;
|
||||
|
||||
// Calculate progress for this equipment spell
|
||||
const eSpellCastSpeed = eSpellDef.castSpeed || 1;
|
||||
const eProgressPerTick = HOURS_PER_TICK * eSpellCastSpeed * totalAttackSpeed;
|
||||
let eCastProgress = (eSpell.castProgress || 0) + eProgressPerTick;
|
||||
// ─── Spell casting (only when a valid spell is configured) ────────────────
|
||||
if (spellDef) {
|
||||
// Compute discipline bonuses once per tick
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
|
||||
// Process complete casts for equipment spells (safety counter prevents infinite loop)
|
||||
let eSafetyCounter = 0;
|
||||
const MAX_E_CASTS_PER_TICK = 100;
|
||||
while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements) && eSafetyCounter < MAX_E_CASTS_PER_TICK) {
|
||||
// Deduct cost
|
||||
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
||||
rawMana = eAfterCost.rawMana;
|
||||
elements = eAfterCost.elements;
|
||||
// Calculate damage — for multi-element guardians, use all elements
|
||||
const eFloorElement = getFloorElement(currentFloor);
|
||||
const eBaseDamage = calcDamage(
|
||||
// Calculate cast speed (no skill bonus)
|
||||
const totalAttackSpeed = attackSpeedMult;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
||||
|
||||
castProgress = (castProgress || 0) + progressPerTick;
|
||||
|
||||
// Process complete casts for active spell (safety counter prevents infinite loop)
|
||||
let safetyCounter = 0;
|
||||
const MAX_CASTS_PER_TICK = 100;
|
||||
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements) && safetyCounter < MAX_CASTS_PER_TICK) {
|
||||
// Deduct spell cost
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
// Calculate base damage (without elemental bonus first)
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
const baseDamage = calcDamage(
|
||||
{ signedPacts },
|
||||
eSpell.spellId,
|
||||
spellId,
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
const eGuardian = getGuardianForFloor(currentFloor);
|
||||
const eFloorElems = eGuardian && eGuardian.element.length > 0
|
||||
? eGuardian.element
|
||||
: [eFloorElement];
|
||||
const eMultiElemBonus = getMultiElementBonus(eSpellDef.elem, eFloorElems);
|
||||
const eDamage = eBaseDamage * eMultiElemBonus;
|
||||
// Apply elemental bonus — for multi-element guardians, use all elements
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
const floorElems = guardian && guardian.element.length > 0
|
||||
? guardian.element
|
||||
: [floorElement];
|
||||
const multiElemBonus = getMultiElementBonus(spellDef.elem, floorElems);
|
||||
const damage = baseDamage * multiElemBonus;
|
||||
|
||||
const eResult = onDamageDealt(eDamage);
|
||||
rawMana = eResult.rawMana;
|
||||
elements = eResult.elements;
|
||||
const eFinalDamage = eResult.modifiedDamage || eDamage;
|
||||
// Let gameStore apply damage modifiers (executioner, berserker)
|
||||
const result = onDamageDealt(damage);
|
||||
rawMana = result.rawMana;
|
||||
elements = result.elements;
|
||||
const finalDamage = result.modifiedDamage || damage;
|
||||
|
||||
// Guard against NaN damage
|
||||
if (!Number.isFinite(eFinalDamage)) {
|
||||
// Guard against NaN damage — if damage is not finite, stop processing
|
||||
if (!Number.isFinite(finalDamage)) {
|
||||
logMessages.push('⚠️ Combat stopped: invalid damage value');
|
||||
break;
|
||||
}
|
||||
|
||||
floorHP = Math.max(0, floorHP - eFinalDamage);
|
||||
eCastProgress -= 1;
|
||||
eSafetyCounter++;
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - finalDamage);
|
||||
castProgress -= 1;
|
||||
safetyCounter++;
|
||||
|
||||
// Apply on-hit effect (DoT/debuff) to enemy (spec §6.2)
|
||||
applyOnHitEffect(get, set, spellId, logMessages);
|
||||
|
||||
// Check if room/floor is cleared
|
||||
if (floorHP <= 0) {
|
||||
const eGuardian = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!eGuardian);
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!guardian);
|
||||
|
||||
// ── Spec: room-aware advancement ─────────────────────────────────
|
||||
// ── Spec: room-aware advancement (climbing spec §4.4) ──────────────
|
||||
// Instead of directly incrementing the floor, delegate to the store's
|
||||
// advanceRoomOrFloor which handles room-by-room and floor transitions.
|
||||
get().advanceRoomOrFloor();
|
||||
|
||||
// Re-read state after advancement
|
||||
const newState = get();
|
||||
currentFloor = newState.currentFloor;
|
||||
floorMaxHP = newState.floorMaxHP;
|
||||
floorHP = newState.floorHP;
|
||||
eCastProgress = 0;
|
||||
if (eGuardian) {
|
||||
logMessages.push(`\u2694\ufe0f ${eGuardian.name} defeated!`);
|
||||
castProgress = 0;
|
||||
|
||||
if (guardian) {
|
||||
logMessages.push(`⚔️ ${guardian.name} defeated!`);
|
||||
} else if (currentFloor % 5 === 0) {
|
||||
logMessages.push(`\ud83c\udff0 Floor ${currentFloor - 1} cleared!`);
|
||||
logMessages.push(`🗺️ Floor ${currentFloor - 1} cleared!`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update equipment spell state
|
||||
updatedEquipmentSpellStates[i] = { ...eSpell, castProgress: eCastProgress % 1 };
|
||||
// Process equipment spell states (for progress bars in UI)
|
||||
for (let i = 0; i < updatedEquipmentSpellStates.length; i++) {
|
||||
const eSpell = updatedEquipmentSpellStates[i];
|
||||
const eSpellDef = SPELLS_DEF[eSpell.spellId];
|
||||
if (!eSpellDef) continue;
|
||||
|
||||
// Calculate progress for this equipment spell
|
||||
const eSpellCastSpeed = eSpellDef.castSpeed || 1;
|
||||
const eProgressPerTick = HOURS_PER_TICK * eSpellCastSpeed * attackSpeedMult;
|
||||
let eCastProgress = (eSpell.castProgress || 0) + eProgressPerTick;
|
||||
|
||||
// Process complete casts for equipment spells (safety counter prevents infinite loop)
|
||||
let eSafetyCounter = 0;
|
||||
const MAX_E_CASTS_PER_TICK = 100;
|
||||
while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements) && eSafetyCounter < MAX_E_CASTS_PER_TICK) {
|
||||
// Deduct cost
|
||||
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
||||
rawMana = eAfterCost.rawMana;
|
||||
elements = eAfterCost.elements;
|
||||
// Calculate damage — for multi-element guardians, use all elements
|
||||
const eFloorElement = getFloorElement(currentFloor);
|
||||
const eBaseDamage = calcDamage(
|
||||
{ signedPacts },
|
||||
eSpell.spellId,
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
const eGuardian = getGuardianForFloor(currentFloor);
|
||||
const eFloorElems = eGuardian && eGuardian.element.length > 0
|
||||
? eGuardian.element
|
||||
: [eFloorElement];
|
||||
const eMultiElemBonus = getMultiElementBonus(eSpellDef.elem, eFloorElems);
|
||||
const eDamage = eBaseDamage * eMultiElemBonus;
|
||||
|
||||
const eResult = onDamageDealt(eDamage);
|
||||
rawMana = eResult.rawMana;
|
||||
elements = eResult.elements;
|
||||
const eFinalDamage = eResult.modifiedDamage || eDamage;
|
||||
|
||||
// Guard against NaN damage
|
||||
if (!Number.isFinite(eFinalDamage)) {
|
||||
break;
|
||||
}
|
||||
|
||||
floorHP = Math.max(0, floorHP - eFinalDamage);
|
||||
eCastProgress -= 1;
|
||||
eSafetyCounter++;
|
||||
|
||||
if (floorHP <= 0) {
|
||||
const eGuardian = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!eGuardian);
|
||||
|
||||
// ── Spec: room-aware advancement ─────────────────────────────────
|
||||
get().advanceRoomOrFloor();
|
||||
|
||||
const newState = get();
|
||||
currentFloor = newState.currentFloor;
|
||||
floorMaxHP = newState.floorMaxHP;
|
||||
floorHP = newState.floorHP;
|
||||
eCastProgress = 0;
|
||||
if (eGuardian) {
|
||||
logMessages.push(`⚔️ ${eGuardian.name} defeated!`);
|
||||
} else if (currentFloor % 5 === 0) {
|
||||
logMessages.push(`🗺️ Floor ${currentFloor - 1} cleared!`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update equipment spell state
|
||||
updatedEquipmentSpellStates[i] = { ...eSpell, castProgress: eCastProgress % 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Golem attacks (spec §9.4) ─────────────────────────────────────────
|
||||
// ─── Melee sword attacks (spec §3.1, §4.3) ────────────────────────────
|
||||
// Melee: no mana cost, no Executioner/Berserker, elemental matchup applies
|
||||
const updatedMeleeSwordProgress = { ...state.meleeSwordProgress };
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
const guardian = getGuardianForFloor(currentFloor);
|
||||
const floorElems = guardian && guardian.element.length > 0 ? guardian.element : [floorElement];
|
||||
|
||||
|
||||
if (equippedSwords && Object.keys(equippedSwords).length > 0 && floorHP > 0) {
|
||||
for (const [instanceId, swordInstance] of Object.entries(equippedSwords)) {
|
||||
const swordType = EQUIPMENT_TYPES[swordInstance.typeId];
|
||||
if (!swordType || !swordType.stats?.attackSpeed) continue;
|
||||
const swordAttackSpeed = swordType.stats.attackSpeed;
|
||||
const meleeProgressPerTick = HOURS_PER_TICK * swordAttackSpeed * attackSpeedMult;
|
||||
let meleeProgress = (updatedMeleeSwordProgress[instanceId] || 0) + meleeProgressPerTick;
|
||||
let meleeSafetyCounter = 0;
|
||||
while (meleeProgress >= 1 && meleeSafetyCounter < 100) {
|
||||
const meleeDamage = calcMeleeDamage(swordInstance as EquipmentInstance, swordType, floorElems[0]);
|
||||
const finalMeleeDamage = applyEnemyDefenses(meleDamage, null, 'combat', (msg) => logMessages.push(msg));
|
||||
if (!Number.isFinite(finalMeleeDamage)) break;
|
||||
floorHP = Math.max(0, floorHP - finalMeleeDamage);
|
||||
meleeProgress -= 1;
|
||||
meleeSafetyCounter++;
|
||||
if (floorHP <= 0) {
|
||||
const g = getGuardianForFloor(currentFloor);
|
||||
onFloorCleared(currentFloor, !!g);
|
||||
get().advanceRoomOrFloor();
|
||||
const ns = get();
|
||||
currentFloor = ns.currentFloor;
|
||||
floorMaxHP = ns.floorMaxHP;
|
||||
floorHP = ns.floorHP;
|
||||
meleeProgress = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updatedMeleeSwordProgress[instanceId] = meleeProgress % 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Golem attacks (spec §9.4) ───────────────────────────────────────────
|
||||
// Golems attack after spells, using the same damage pipeline.
|
||||
// They ignore Executioner/Berserker (handled internally by processGolemAttacks).
|
||||
if (activeGolems.length > 0 && floorHP > 0) {
|
||||
@@ -315,7 +330,7 @@ export function processCombatTick(
|
||||
currentFloor = postGolemState.currentFloor;
|
||||
}
|
||||
|
||||
// ─── DoT/Debuff tick processing (spec §6.3) ──────────────────────────
|
||||
// ─── DoT/Debuff tick processing (spec §6.3) ─────────────────────────────
|
||||
// Process after all weapon/golem attacks
|
||||
if (floorHP > 0) {
|
||||
const doTDamage = processDoTPhase(get, set, applyEnemyDefenses, logMessages);
|
||||
@@ -347,12 +362,13 @@ export function processCombatTick(
|
||||
castProgress,
|
||||
equipmentSpellStates: updatedEquipmentSpellStates,
|
||||
activeGolems,
|
||||
meleeSwordProgress: updatedMeleeSwordProgress,
|
||||
};
|
||||
} catch (error) {
|
||||
// Return safe defaults on error — combat tick should never crash the game
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logMessages.push(`⚠️ Combat error: ${errorMsg}`);
|
||||
return makeDefaultCombatTickResult(rawMana, elements, state, activeGolems);
|
||||
return makeDefaultCombatTickResult(rawMana, elements, state, golemancyState.activeGolems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,4 +384,4 @@ export function makeInitialSpells(spellsToKeep: string[] = []): Record<string, S
|
||||
}
|
||||
|
||||
return startSpells;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user