feat: implement golemancy combat system (spec §6, §9)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

- Add ActiveGolem interface and activeGolems to GolemancyState
- Add maxRoomDuration to all 12 golem definitions
- Create golem-combat-actions.ts with pure golem combat logic (summoning, maintenance, attacks, room-duration)
- Create golem-combat.ts pipeline for golem combat setup
- Wire golem maintenance and attacks into processCombatTick
- Wire golem summoning into advanceRoomOrFloor on room entry
- Wire golem room-duration countdown into onFloorCleared callback
- Update combat-actions tests for new processCombatTick signature
- All 921 tests pass, all files under 400-line limit

Closes #259
This commit is contained in:
2026-06-03 15:40:39 +02:00
parent 7c0e740226
commit a2cdf6d21c
20 changed files with 583 additions and 41 deletions
+10 -27
View File
@@ -23,6 +23,8 @@ import { createSafeStorage } from '../utils/safe-persist';
import { createStartNewLoop } from './gameLoopActions';
import { buildTickContext, applyTickWrites } from './tick-pipeline';
import { processEnchantingTicks } from './pipelines/enchanting-tick';
import { buildGolemCombatPipeline } from './pipelines/golem-combat';
import type { TickContext, TickWrites } from './tick-pipeline';
import type { GameCoordinatorState } from './gameStore.types';
import type { EnemyState } from '../types';
@@ -294,44 +296,25 @@ export const useGameStore = create<GameCoordinatorStore>()(
// Combat — delegate to combatStore
if (ctx.combat.currentAction === 'climb') {
const combatCbs = buildCombatCallbacks({
ctx, effects, maxMana, addLog, useCombatStore, usePrestigeStore,
});
// Mage barrier recharge (spec §5.2) — recharge before building defense ctx
const combatCbs = buildCombatCallbacks({ ctx, effects, maxMana, addLog, useCombatStore, usePrestigeStore });
const roomEnemies = ctx.combat.currentRoom?.enemies ?? [];
const primaryEnemy = roomEnemies[0] ?? null;
const rechargedEnemy = combatCbs.applyMageBarrierRecharge(primaryEnemy);
const activeEnemy = rechargedEnemy ?? primaryEnemy;
// Build enemy defense context for this tick (spec §5.2)
const defCtx = {
roomType: ctx.combat.currentRoom?.roomType ?? 'combat',
enemy: activeEnemy,
};
const activeEnemy = combatCbs.applyMageBarrierRecharge(primaryEnemy) ?? primaryEnemy;
const defCtx = { roomType: ctx.combat.currentRoom?.roomType ?? 'combat', enemy: activeEnemy };
const golemPipeline = buildGolemCombatPipeline(addLog);
const combatResult = useCombatStore.getState().processCombatTick(
rawMana, elements, maxMana, 1,
combatCbs.onFloorCleared,
combatCbs.makeOnDamageDealt(() => rawMana, () => elements, defCtx, addLog),
ctx.prestige.signedPacts,
{ activeGolems: golemPipeline.activeGolems },
golemPipeline.golemApplyDamageToRoom,
);
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,
};
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 } };
}
if (ctx.combat.currentAction === 'craft') {