feat: add fabricator disciplines for recipe unlocks
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- Add unlocksRecipes field to DisciplinePerk type
- Add study-fabricator-recipes discipline (earth/metal/sand/crystal recipes)
- Add study-wizard-branch discipline (wizard equipment recipes)
- Add study-physical-branch discipline (physical combat equipment recipes)
- Collect unlocksRecipes during discipline tick processing
- Pass recipe unlocks to crafting store via gameStore
- Add unlockRecipes action to craftingStore with persistence
- Filter fabricator recipes by unlock status in FabricatorSubTab UI
This commit is contained in:
2026-05-29 15:42:09 +02:00
parent 9e49aa1ca6
commit 644b76f16d
10 changed files with 216 additions and 8 deletions
@@ -19,6 +19,7 @@ export function createDefaultCraftingState(): CraftingState {
equipmentCraftingProgress: null,
enchantmentDesigns: [],
unlockedEffects: [],
unlockedRecipes: [],
equippedInstances: initial.equippedInstances,
equipmentInstances: initial.instances,
lootInventory: {
+16
View File
@@ -344,6 +344,21 @@ export const useCraftingStore = create<CraftingStore>()(
});
},
unlockRecipes: (recipeIds: string[]) => {
set((state) => {
const existing = new Set(state.unlockedRecipes);
let changed = false;
for (const id of recipeIds) {
if (!existing.has(id)) {
existing.add(id);
changed = true;
}
}
if (!changed) return state;
return { unlockedRecipes: Array.from(existing) };
});
},
processEquipmentCraftingTick: (): { completed: boolean; logMessage?: string } => {
const state = get();
return processEquipmentCraftingTick(state, set as unknown as (partial: Partial<CraftingState>) => void);
@@ -366,6 +381,7 @@ export const useCraftingStore = create<CraftingStore>()(
equipmentCraftingProgress: state.equipmentCraftingProgress,
enchantmentDesigns: state.enchantmentDesigns,
unlockedEffects: state.unlockedEffects,
unlockedRecipes: state.unlockedRecipes,
equipmentInstances: state.equipmentInstances,
equippedInstances: state.equippedInstances,
lootInventory: state.lootInventory,
@@ -24,6 +24,7 @@ export interface CraftingState {
equipmentCraftingProgress: EquipmentCraftingProgress | null;
enchantmentDesigns: EnchantmentDesign[];
unlockedEffects: string[];
unlockedRecipes: string[];
equipmentInstances: Record<string, EquipmentInstance>;
equippedInstances: Record<string, string | null>;
lootInventory: {
@@ -72,6 +73,7 @@ export interface CraftingActions {
resetEnchantmentSelection: () => void;
clearLastError: () => void;
unlockEffects: (effectIds: string[]) => void;
unlockRecipes: (recipeIds: string[]) => void;
processEquipmentCraftingTick: () => { completed: boolean; logMessage?: string };
resetCrafting: () => void;
}
+10 -3
View File
@@ -54,6 +54,7 @@ export interface DisciplineStoreActions {
rawMana: number;
elements: Record<string, ElementState>;
unlockedEffects: string[];
unlockedRecipes: string[];
};
setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void;
resetDisciplines: () => void;
@@ -158,6 +159,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
let newXP = s.totalXP;
const newDisciplines = { ...s.disciplines };
const newUnlockedEffects: string[] = [];
const newUnlockedRecipes: string[] = [];
const newProcessedPerks = [...s.processedPerks];
const drainedIds: string[] = [];
@@ -221,10 +223,15 @@ export const useDisciplineStore = create<DisciplineStore>()(
const newPerks = getUnlockedPerks(def, disc.xp + 1);
const oldPerkIds = new Set(oldPerks.map(p => p.id));
for (const perk of newPerks) {
if (!oldPerkIds.has(perk.id) && perk.unlocksEffects) {
if (!oldPerkIds.has(perk.id)) {
const perkKey = `${id}:${perk.id}`;
if (!newProcessedPerks.includes(perkKey)) {
newUnlockedEffects.push(...perk.unlocksEffects);
if (perk.unlocksEffects) {
newUnlockedEffects.push(...perk.unlocksEffects);
}
if (perk.unlocksRecipes) {
newUnlockedRecipes.push(...perk.unlocksRecipes);
}
newProcessedPerks.push(perkKey);
}
}
@@ -251,7 +258,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
processedPerks: newProcessedPerks,
});
return { rawMana, elements, unlockedEffects: newUnlockedEffects };
return { rawMana, elements, unlockedEffects: newUnlockedEffects, unlockedRecipes: newUnlockedRecipes };
},
}),
{ storage: createSafeStorage(), name: 'mana-loop-discipline-store', version: 1, partialize: (state) => ({ disciplines: state.disciplines, activeIds: state.activeIds, concurrentLimit: state.concurrentLimit, totalXP: state.totalXP, processedPerks: state.processedPerks }) }
+8
View File
@@ -277,6 +277,14 @@ export const useGameStore = create<GameCoordinatorStore>()(
}
}
// Unlock fabricator recipes from newly unlocked discipline perks
if (disciplineResult.unlockedRecipes.length > 0) {
useCraftingStore.getState().unlockRecipes(disciplineResult.unlockedRecipes);
for (const recipeId of disciplineResult.unlockedRecipes) {
addLog(`🔨 Fabricator recipe unlocked: ${recipeId}`);
}
}
// Apply per-element capacity bonuses from disciplines and equipment
const perElementCapBonuses = mergePerElementCapBonuses(
disciplineEffects.bonuses,