fix: resolve 7 medium-priority bugs from audit #372
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
- #371: Replace Math.random() with seeded PRNG in getSpireEnemyArmor/Barrier - #370: Add mana refund when cancelling pact ritual in cancelPactRitual - #367: Add ENCHANT_MASTERY check for design slot 2 in crafting store - #364: Fix useGameDerived to read crafting data from useCraftingStore - #363: Clamp recovery room regen delta to prevent negative mana loss - #365: Add shield/barrier/healthRegen fields to all procedural guardians - #362: Refactor enchanting tick pipeline to return writes instead of direct store calls Extracted procedural guardian generators into guardian-procedural.ts to stay under 400-line limit. All 1158 tests pass.
This commit is contained in:
@@ -10,6 +10,7 @@ import { useCombatStore } from './combatStore';
|
||||
|
||||
import { useUIStore } from './uiStore';
|
||||
import { getEnchantingEfficiencyBonus } from '../effects/discipline-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
||||
import * as ApplicationActions from '../crafting-actions/application-actions';
|
||||
import * as PreparationActions from '../crafting-actions/preparation-actions';
|
||||
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions';
|
||||
@@ -19,6 +20,7 @@ import { createDefaultCraftingState } from './crafting-initial-state';
|
||||
import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
|
||||
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
|
||||
import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting';
|
||||
import { computeEffects } from '../effects/upgrade-effects';
|
||||
|
||||
export const useCraftingStore = create<CraftingStore>()(
|
||||
persist(
|
||||
@@ -65,6 +67,10 @@ export const useCraftingStore = create<CraftingStore>()(
|
||||
// Update currentAction in combatStore
|
||||
useCombatStore.setState({ currentAction: 'design' });
|
||||
} else if (!state.designProgress2) {
|
||||
// Check for Enchant Mastery before allowing second design slot
|
||||
const computedEffects = computeEffects();
|
||||
const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY);
|
||||
if (!hasEnchantMastery) return false;
|
||||
updates = {
|
||||
designProgress2: {
|
||||
designId: CraftingUtils.generateDesignId(),
|
||||
|
||||
@@ -317,7 +317,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
// the delta (9× additional) to avoid double-counting.
|
||||
const boostedRegen = baseRegen * 10;
|
||||
const netBoostedRegen = Math.max(0, boostedRegen * (1 - incursionStrength) * meditationMultiplier - conversionResult.totalRawDrain);
|
||||
const regenDelta = netBoostedRegen - netRawRegen;
|
||||
const regenDelta = Math.max(0, netBoostedRegen - netRawRegen);
|
||||
rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana);
|
||||
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
||||
if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue;
|
||||
@@ -351,6 +351,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
if (enchantingResult.writes.attunement) {
|
||||
writes.attunement = { ...(writes.attunement || {}), ...enchantingResult.writes.attunement };
|
||||
}
|
||||
if (enchantingResult.writes.crafting) {
|
||||
writes.crafting = { ...(writes.crafting || {}), ...enchantingResult.writes.crafting };
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Write
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// ─── 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.
|
||||
//
|
||||
// IMPORTANT: This function reads from the ctx snapshot and returns writes.
|
||||
// It must NOT call useCraftingStore.setState() directly — that would bypass
|
||||
// the tick pipeline's snapshot-and-batch-write pattern and cause race conditions.
|
||||
|
||||
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 { getEnchantingEfficiencyBonus } from '../../effects/discipline-effects';
|
||||
import type { TickContext, TickWrites } from '../tick-pipeline';
|
||||
|
||||
@@ -26,6 +29,11 @@ export function processEnchantingTicks(
|
||||
const writes: Partial<TickWrites> = {};
|
||||
const currentAction = ctx.combat.currentAction;
|
||||
|
||||
// Helper to merge crafting writes
|
||||
const mergeCrafting = (partial: Partial<typeof ctx.crafting>) => {
|
||||
writes.crafting = { ...(writes.crafting || {}), ...partial } as typeof writes.crafting;
|
||||
};
|
||||
|
||||
// ── Phase 1: Design ──────────────────────────────────────────────────────
|
||||
if (currentAction === 'design') {
|
||||
const designProgress = ctx.crafting.designProgress;
|
||||
@@ -54,19 +62,29 @@ export function processEnchantingTicks(
|
||||
},
|
||||
getEnchantingEfficiencyBonus(),
|
||||
);
|
||||
useCraftingStore.getState().saveDesign(completedDesign);
|
||||
// Return write instead of calling store directly
|
||||
mergeCrafting({
|
||||
enchantmentDesigns: [...ctx.crafting.enchantmentDesigns, completedDesign],
|
||||
designProgress: designProgress ? null : undefined,
|
||||
designProgress2: designProgress2 ? null : undefined,
|
||||
});
|
||||
addLog('Design "' + completedDesign.name + '" completed!');
|
||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||
} else {
|
||||
// Return write instead of calling store directly
|
||||
if (designProgress) {
|
||||
useCraftingStore.getState().setDesignProgress({
|
||||
...designProgress,
|
||||
progress: designResult.progress,
|
||||
mergeCrafting({
|
||||
designProgress: {
|
||||
...designProgress,
|
||||
progress: designResult.progress,
|
||||
},
|
||||
});
|
||||
} else if (designProgress2) {
|
||||
useCraftingStore.getState().setDesignProgress2({
|
||||
...designProgress2,
|
||||
progress: designResult.progress,
|
||||
mergeCrafting({
|
||||
designProgress2: {
|
||||
...designProgress2,
|
||||
progress: designResult.progress,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -81,7 +99,7 @@ export function processEnchantingTicks(
|
||||
} else {
|
||||
const instance = ctx.crafting.equipmentInstances[prepProgress.equipmentInstanceId];
|
||||
if (!instance) {
|
||||
useCraftingStore.getState().setPreparationProgress(null);
|
||||
mergeCrafting({ preparationProgress: null });
|
||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||
addLog('Preparation failed: equipment not found.');
|
||||
} else {
|
||||
@@ -97,20 +115,21 @@ export function processEnchantingTicks(
|
||||
}
|
||||
if (prepResult.isComplete) {
|
||||
const completionResult = completePreparation(instance);
|
||||
useCraftingStore.setState((s) => ({
|
||||
equipmentInstances: {
|
||||
...s.equipmentInstances,
|
||||
[prepProgress.equipmentInstanceId]: completionResult.updatedInstance,
|
||||
},
|
||||
const newInstances = { ...ctx.crafting.equipmentInstances };
|
||||
newInstances[prepProgress.equipmentInstanceId] = completionResult.updatedInstance;
|
||||
mergeCrafting({
|
||||
equipmentInstances: newInstances,
|
||||
preparationProgress: null,
|
||||
}));
|
||||
});
|
||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||
addLog(completionResult.logMessage);
|
||||
} else {
|
||||
useCraftingStore.getState().setPreparationProgress({
|
||||
...prepProgress,
|
||||
progress: prepResult.progress,
|
||||
manaCostPaid: prepResult.manaCostPaid,
|
||||
mergeCrafting({
|
||||
preparationProgress: {
|
||||
...prepProgress,
|
||||
progress: prepResult.progress,
|
||||
manaCostPaid: prepResult.manaCostPaid,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -128,7 +147,7 @@ export function processEnchantingTicks(
|
||||
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);
|
||||
mergeCrafting({ applicationProgress: null });
|
||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||
addLog('Enchantment failed: equipment or design not found.');
|
||||
} else {
|
||||
@@ -146,13 +165,12 @@ export function processEnchantingTicks(
|
||||
}
|
||||
if (appResult.isComplete) {
|
||||
const applyResult = applyEnchantments(instance, design, effects);
|
||||
useCraftingStore.setState((s) => ({
|
||||
equipmentInstances: {
|
||||
...s.equipmentInstances,
|
||||
[appProgress.equipmentInstanceId]: applyResult.updatedInstance,
|
||||
},
|
||||
const newInstances = { ...ctx.crafting.equipmentInstances };
|
||||
newInstances[appProgress.equipmentInstanceId] = applyResult.updatedInstance;
|
||||
mergeCrafting({
|
||||
equipmentInstances: newInstances,
|
||||
applicationProgress: null,
|
||||
}));
|
||||
});
|
||||
const updatedAttunements = updateEnchanterAttunement(
|
||||
ctx.attunement.attunements,
|
||||
applyResult.xpGained,
|
||||
@@ -164,10 +182,12 @@ export function processEnchantingTicks(
|
||||
addLog('Free enchantment triggered! No mana consumed this tick.');
|
||||
}
|
||||
} else {
|
||||
useCraftingStore.getState().setApplicationProgress({
|
||||
...appProgress,
|
||||
progress: appResult.progress,
|
||||
manaSpent: appResult.manaSpent,
|
||||
mergeCrafting({
|
||||
applicationProgress: {
|
||||
...appProgress,
|
||||
progress: appResult.progress,
|
||||
manaSpent: appResult.manaSpent,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,15 @@ export const usePrestigeStore = create<PrestigeStore>()(
|
||||
},
|
||||
|
||||
cancelPactRitual: () => {
|
||||
const state = get();
|
||||
if (state.pactRitualFloor !== null) {
|
||||
const guardian = getGuardianForFloor(state.pactRitualFloor);
|
||||
if (guardian) {
|
||||
useManaStore.setState((s) => ({
|
||||
rawMana: s.rawMana + guardian.pactCost,
|
||||
}));
|
||||
}
|
||||
}
|
||||
set({
|
||||
pactRitualFloor: null,
|
||||
pactRitualProgress: 0,
|
||||
|
||||
Reference in New Issue
Block a user