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

- 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:
2026-06-03 21:59:30 +02:00
parent b506f0bcc3
commit 8dde423526
10 changed files with 564 additions and 183 deletions
+16 -2
View File
@@ -1,7 +1,7 @@
// Game Store — coordinator, tick pipeline, time/incursion
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
import { HOURS_PER_TICK, MAX_DAY, EQUIPMENT_TYPES } from '../constants';
import { computeEquipmentEffects } from '../effects';
import type { ComputedEffects } from '../effects/upgrade-effects.types';
@@ -303,6 +303,19 @@ export const useGameStore = create<GameCoordinatorStore>()(
const activeEnemy = combatCbs.applyMageBarrierRecharge(primaryEnemy) ?? primaryEnemy;
const defCtx = { roomType: ctx.combat.currentRoom?.roomType ?? 'combat', enemy: activeEnemy };
const golemPipeline = buildGolemCombatPipeline(addLog);
// Build equipped swords map for melee auto-attack (spec §3.1)
const equippedSwords: Record<string, import('../types').EquipmentInstance> = {};
for (const [slot, instanceId] of Object.entries(ctx.crafting.equippedInstances || {})) {
if (!instanceId) continue;
const inst = ctx.crafting.equipmentInstances?.[instanceId];
if (!inst) continue;
const eqType = EQUIPMENT_TYPES[inst.typeId];
if (eqType?.category === 'sword') {
equippedSwords[instanceId] = inst;
}
}
const combatResult = useCombatStore.getState().processCombatTick(
rawMana, elements, maxMana, 1,
combatCbs.onFloorCleared,
@@ -313,12 +326,13 @@ export const useGameStore = create<GameCoordinatorStore>()(
(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) => applyEnemyDefensesFromPipeline(
dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier,
),
equippedSwords,
);
rawMana = combatResult.rawMana;
elements = combatResult.elements;
totalManaGathered += combatResult.totalManaGathered || 0;
if (combatResult.logMessages) combatResult.logMessages.forEach(msg => addLog(msg));
writes.combat = { ...(writes.combat || {}), currentFloor: combatResult.currentFloor, floorHP: combatResult.floorHP, floorMaxHP: combatResult.floorMaxHP, maxFloorReached: combatResult.maxFloorReached, castProgress: combatResult.castProgress, equipmentSpellStates: combatResult.equipmentSpellStates, golemancy: { ...(writes.combat?.golemancy || ctx.combat.golemancy), activeGolems: combatResult.activeGolems } };
writes.combat = { ...(writes.combat || {}), currentFloor: combatResult.currentFloor, floorHP: combatResult.floorHP, floorMaxHP: combatResult.floorMaxHP, maxFloorReached: combatResult.maxFloorReached, castProgress: combatResult.castProgress, equipmentSpellStates: combatResult.equipmentSpellStates, golemancy: { ...(writes.combat?.golemancy || ctx.combat.golemancy), activeGolems: combatResult.activeGolems }, meleeSwordProgress: combatResult.meleeSwordProgress };
}
if (ctx.combat.currentAction === 'craft') {