fix: multiple bug fixes - infinite loop crash, enchant tick handlers, discipline crash, Plasma symbol, desync
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s

- #236: Fix Climb the Spire React #185 infinite loop - removed redundant set() in processCombatTick that caused double Zustand writes per tick
- #235: Add enchanting design/prepare/apply tick handlers - extracted to pipelines/enchanting-tick.ts
- #235: Fix startApplying not setting currentAction to 'enchant'
- #243: Guard discipline store against undefined activeIds/processedPerks from corrupted persisted state
- #245: Change Plasma symbol from  (conflicts with Lightning) to 🔴
- #241: Fix combat store maxFloorReached desync - initialize to 0, reset on exitSpireMode
- #239: Fix EffectSelector not rendering when unlockedEffects is empty (fresh game)
- Created pipelines/enchanting-tick.ts to keep gameStore.ts under 400 lines
This commit is contained in:
2026-06-01 12:57:52 +02:00
parent 2539559edc
commit 7dd9ad5b92
11 changed files with 213 additions and 21 deletions
@@ -0,0 +1,177 @@
// ─── Enchanting Tick Handlers ─────────────────────────────────────────────────
// Design → Prepare → Application tick processing for the enchanting pipeline.
// Extracted from gameStore.ts to keep the coordinator under the 400-line limit.
import { HOURS_PER_TICK } from '../../constants';
import type { ComputedEffects } from '../../effects/upgrade-effects.types';
import { calculateDesignProgress, createCompletedDesignFromProgress } from '../../crafting-design';
import { calculatePreparationTick, completePreparation } from '../../crafting-prep';
import { calculateApplicationTick, applyEnchantments, updateEnchanterAttunement } from '../../crafting-apply';
import { useCraftingStore } from '../craftingStore';
import type { TickContext, TickWrites } from '../tick-pipeline';
interface EnchantingTickParams {
ctx: TickContext;
effects: ComputedEffects;
rawMana: number;
addLog: (msg: string) => void;
}
export function processEnchantingTicks(
params: EnchantingTickParams,
): { rawMana: number; writes: Partial<TickWrites> } {
const { ctx, effects, rawMana: initialRawMana, addLog } = params;
let rawMana = initialRawMana;
const writes: Partial<TickWrites> = {};
const currentAction = ctx.combat.currentAction;
// ── Phase 1: Design ──────────────────────────────────────────────────────
if (currentAction === 'design') {
const designProgress = ctx.crafting.designProgress;
const designProgress2 = ctx.crafting.designProgress2;
if (!designProgress && !designProgress2) {
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
} else {
const activeProgress = designProgress || designProgress2!;
const isRepeatDesign = ctx.crafting.enchantmentDesigns.some(
(d) => d.name === activeProgress.name,
);
const designResult = calculateDesignProgress(
activeProgress.progress,
activeProgress.required,
effects,
isRepeatDesign,
);
if (designResult.isComplete) {
const completedDesign = createCompletedDesignFromProgress(
{
designId: activeProgress.designId,
name: activeProgress.name,
equipmentType: activeProgress.equipmentType,
effects: activeProgress.effects,
required: activeProgress.required,
},
0,
);
useCraftingStore.getState().saveDesign(completedDesign);
addLog('Design "' + completedDesign.name + '" completed!');
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
} else {
if (designProgress) {
useCraftingStore.getState().setDesignProgress({
...designProgress,
progress: designResult.progress,
});
} else if (designProgress2) {
useCraftingStore.getState().setDesignProgress2({
...designProgress2,
progress: designResult.progress,
});
}
}
}
}
// ── Phase 2: Preparation ─────────────────────────────────────────────────
if (currentAction === 'prepare') {
const prepProgress = ctx.crafting.preparationProgress;
if (!prepProgress) {
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
} else {
const instance = ctx.crafting.equipmentInstances[prepProgress.equipmentInstanceId];
if (!instance) {
useCraftingStore.getState().setPreparationProgress(null);
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
addLog('Preparation failed: equipment not found.');
} else {
const manaPerTick = (instance.totalCapacity * 10) / (2 + instance.totalCapacity / 50) * HOURS_PER_TICK;
const prepResult = calculatePreparationTick(
prepProgress.progress,
prepProgress.required,
prepProgress.manaCostPaid,
manaPerTick,
);
if (prepResult.manaConsumed > 0) {
rawMana = Math.max(0, rawMana - prepResult.manaConsumed);
}
if (prepResult.isComplete) {
const completionResult = completePreparation(instance);
useCraftingStore.setState((s) => ({
equipmentInstances: {
...s.equipmentInstances,
[prepProgress.equipmentInstanceId]: completionResult.updatedInstance,
},
preparationProgress: null,
}));
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
addLog(completionResult.logMessage);
} else {
useCraftingStore.getState().setPreparationProgress({
...prepProgress,
progress: prepResult.progress,
manaCostPaid: prepResult.manaCostPaid,
});
}
}
}
}
// ── Phase 3: Application ─────────────────────────────────────────────────
if (currentAction === 'enchant') {
const appProgress = ctx.crafting.applicationProgress;
if (!appProgress || appProgress.paused) {
if (!appProgress) {
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
}
} else {
const instance = ctx.crafting.equipmentInstances[appProgress.equipmentInstanceId];
const design = ctx.crafting.enchantmentDesigns.find((d) => d.id === appProgress.designId);
if (!instance || !design) {
useCraftingStore.getState().setApplicationProgress(null);
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
addLog('Enchantment failed: equipment or design not found.');
} else {
const totalStacks = design.effects.reduce((s, e) => s + e.stacks, 0);
const manaPerTick = (20 + 5 * totalStacks) * HOURS_PER_TICK;
const appResult = calculateApplicationTick(
appProgress.progress,
appProgress.required,
appProgress.manaSpent,
manaPerTick,
effects,
);
if (appResult.manaConsumed > 0) {
rawMana = Math.max(0, rawMana - appResult.manaConsumed);
}
if (appResult.isComplete) {
const applyResult = applyEnchantments(instance, design, effects);
useCraftingStore.setState((s) => ({
equipmentInstances: {
...s.equipmentInstances,
[appProgress.equipmentInstanceId]: applyResult.updatedInstance,
},
applicationProgress: null,
}));
const updatedAttunements = updateEnchanterAttunement(
ctx.attunement.attunements,
applyResult.xpGained,
);
writes.attunement = { ...(writes.attunement || {}), attunements: updatedAttunements };
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
addLog(applyResult.logMessage);
if (appResult.triggeredFreeEnchant) {
addLog('Free enchantment triggered! No mana consumed this tick.');
}
} else {
useCraftingStore.getState().setApplicationProgress({
...appProgress,
progress: appResult.progress,
manaSpent: appResult.manaSpent,
});
}
}
}
}
return { rawMana, writes };
}