diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index e3bf758..841ea77 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-05-29T13:10:03.724Z +Generated: 2026-05-29T13:23:45.664Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index f840c48..e342804 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-29T13:10:02.096Z", + "generated": "2026-05-29T13:23:43.981Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx b/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx index c83d741..ee84da4 100644 --- a/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx +++ b/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx @@ -136,6 +136,7 @@ export function FabricatorSubTab() { const [branchFilter, setBranchFilter] = useState('all'); const lootInventory = useCraftingStore((s) => s.lootInventory); + const unlockedRecipes = useCraftingStore((s) => s.unlockedRecipes); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress); const rawMana = useManaStore((s) => s.rawMana); const elements = useManaStore((s) => s.elements); @@ -156,8 +157,8 @@ export function FabricatorSubTab() { } else if (branchFilter === 'physical') { recipes = recipes.filter(r => isPhysicalBranch(r)); } - return recipes.filter(r => r.manaType === selectedManaType); - }, [selectedManaType, branchFilter]); + return recipes.filter(r => r.manaType === selectedManaType && unlockedRecipes.includes(r.id)); + }, [selectedManaType, branchFilter, unlockedRecipes]); const isCrafting = equipmentCraftingProgress !== null; diff --git a/src/lib/game/data/disciplines/fabricator.ts b/src/lib/game/data/disciplines/fabricator.ts index c819007..c592f9c 100644 --- a/src/lib/game/data/disciplines/fabricator.ts +++ b/src/lib/game/data/disciplines/fabricator.ts @@ -57,4 +57,176 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [ }, ], }, -]; \ No newline at end of file + { + id: 'study-fabricator-recipes', + name: 'Study Fabricator Recipes', + attunement: DisciplinesAttunementType.FABRICATOR, + manaType: 'earth', + baseCost: 10, + description: 'Learn to craft elemental equipment at the fabricator.', + statBonus: { stat: 'enchantPower', baseValue: 3, label: 'Enchantment Power' }, + difficultyFactor: 100, + scalingFactor: 80, + drainBase: 2, + perks: [ + { + id: 'fabricator-earth-recipes', + type: 'once', + threshold: 50, + value: 0, + description: 'Unlock Earth fabricator recipes', + unlocksRecipes: ['earthHelm', 'earthChest', 'earthBoots'], + }, + { + id: 'fabricator-metal-recipes', + type: 'once', + threshold: 100, + value: 0, + description: 'Unlock Metal fabricator recipes', + unlocksRecipes: ['metalBlade', 'metalShield', 'metalGloves'], + }, + { + id: 'fabricator-sand-recipes', + type: 'once', + threshold: 150, + value: 0, + description: 'Unlock Sand fabricator recipes', + unlocksRecipes: ['sandBoots', 'sandGloves', 'sandVest'], + }, + { + id: 'fabricator-crystal-recipes', + type: 'once', + threshold: 200, + value: 0, + description: 'Unlock Crystal fabricator recipes', + unlocksRecipes: ['crystalWand', 'crystalRing', 'crystalAmulet'], + }, + ], + }, + { + id: 'study-wizard-branch', + name: 'Study Wizard Equipment', + attunement: DisciplinesAttunementType.FABRICATOR, + manaType: 'earth', + baseCost: 15, + description: 'Learn to craft advanced wizard equipment.', + statBonus: { stat: 'enchantPower', baseValue: 5, label: 'Enchantment Power' }, + difficultyFactor: 150, + scalingFactor: 100, + drainBase: 3, + requires: ['study-fabricator-recipes'], + perks: [ + { + id: 'wizard-oak-staff', + type: 'once', + threshold: 50, + value: 0, + description: 'Unlock Oak Staff recipe', + unlocksRecipes: ['oakStaff'], + }, + { + id: 'wizard-arcanist-staff', + type: 'once', + threshold: 100, + value: 0, + description: 'Unlock Arcanist Staff recipe', + unlocksRecipes: ['arcanistStaff'], + }, + { + id: 'wizard-battlestaff', + type: 'once', + threshold: 150, + value: 0, + description: 'Unlock Battlestaff recipe', + unlocksRecipes: ['battlestaff'], + }, + { + id: 'wizard-arcanist-set', + type: 'once', + threshold: 200, + value: 0, + description: 'Unlock Arcanist Circlet and Robe recipes', + unlocksRecipes: ['arcanistCirclet', 'arcanistRobe'], + }, + { + id: 'wizard-void-catalyst', + type: 'once', + threshold: 250, + value: 0, + description: 'Unlock Void Catalyst recipe', + unlocksRecipes: ['voidCatalyst'], + }, + { + id: 'wizard-arcanist-pendant', + type: 'once', + threshold: 300, + value: 0, + description: 'Unlock Arcanist Pendant recipe', + unlocksRecipes: ['arcanistPendant'], + }, + ], + }, + { + id: 'study-physical-branch', + name: 'Study Physical Equipment', + attunement: DisciplinesAttunementType.FABRICATOR, + manaType: 'earth', + baseCost: 15, + description: 'Learn to craft advanced physical combat equipment.', + statBonus: { stat: 'enchantPower', baseValue: 5, label: 'Enchantment Power' }, + difficultyFactor: 150, + scalingFactor: 100, + drainBase: 3, + requires: ['study-fabricator-recipes'], + perks: [ + { + id: 'physical-crystal-blade', + type: 'once', + threshold: 50, + value: 0, + description: 'Unlock Crystal Blade recipe', + unlocksRecipes: ['crystalBlade'], + }, + { + id: 'physical-arcanist-blade', + type: 'once', + threshold: 100, + value: 0, + description: 'Unlock Arcanist Blade recipe', + unlocksRecipes: ['arcanistBlade'], + }, + { + id: 'physical-void-blade', + type: 'once', + threshold: 150, + value: 0, + description: 'Unlock Void Blade recipe', + unlocksRecipes: ['voidBlade'], + }, + { + id: 'physical-battle-set', + type: 'once', + threshold: 200, + value: 0, + description: 'Unlock Battle Helm and Robe recipes', + unlocksRecipes: ['battleHelm', 'battleRobe'], + }, + { + id: 'physical-battle-boots', + type: 'once', + threshold: 250, + value: 0, + description: 'Unlock Battle Boots recipe', + unlocksRecipes: ['battleBoots'], + }, + { + id: 'physical-combat-gauntlets', + type: 'once', + threshold: 300, + value: 0, + description: 'Unlock Combat Gauntlets recipe', + unlocksRecipes: ['combatGauntlets'], + }, + ], + }, +]; diff --git a/src/lib/game/stores/crafting-initial-state.ts b/src/lib/game/stores/crafting-initial-state.ts index 1942e4a..879cc34 100644 --- a/src/lib/game/stores/crafting-initial-state.ts +++ b/src/lib/game/stores/crafting-initial-state.ts @@ -19,6 +19,7 @@ export function createDefaultCraftingState(): CraftingState { equipmentCraftingProgress: null, enchantmentDesigns: [], unlockedEffects: [], + unlockedRecipes: [], equippedInstances: initial.equippedInstances, equipmentInstances: initial.instances, lootInventory: { diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index 2fef9fc..193e414 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -344,6 +344,21 @@ export const useCraftingStore = create()( }); }, + 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) => void); @@ -366,6 +381,7 @@ export const useCraftingStore = create()( equipmentCraftingProgress: state.equipmentCraftingProgress, enchantmentDesigns: state.enchantmentDesigns, unlockedEffects: state.unlockedEffects, + unlockedRecipes: state.unlockedRecipes, equipmentInstances: state.equipmentInstances, equippedInstances: state.equippedInstances, lootInventory: state.lootInventory, diff --git a/src/lib/game/stores/craftingStore.types.ts b/src/lib/game/stores/craftingStore.types.ts index 8e9569e..8b543a7 100644 --- a/src/lib/game/stores/craftingStore.types.ts +++ b/src/lib/game/stores/craftingStore.types.ts @@ -24,6 +24,7 @@ export interface CraftingState { equipmentCraftingProgress: EquipmentCraftingProgress | null; enchantmentDesigns: EnchantmentDesign[]; unlockedEffects: string[]; + unlockedRecipes: string[]; equipmentInstances: Record; equippedInstances: Record; 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; } diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts index 685acd7..3ac5a39 100644 --- a/src/lib/game/stores/discipline-slice.ts +++ b/src/lib/game/stores/discipline-slice.ts @@ -54,6 +54,7 @@ export interface DisciplineStoreActions { rawMana: number; elements: Record; unlockedEffects: string[]; + unlockedRecipes: string[]; }; setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void; resetDisciplines: () => void; @@ -158,6 +159,7 @@ export const useDisciplineStore = create()( 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()( 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()( 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 }) } diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 8fa8bf0..01a5a32 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -277,6 +277,14 @@ export const useGameStore = create()( } } + // 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, diff --git a/src/lib/game/types/disciplines.ts b/src/lib/game/types/disciplines.ts index 91ca269..df63471 100644 --- a/src/lib/game/types/disciplines.ts +++ b/src/lib/game/types/disciplines.ts @@ -25,6 +25,7 @@ export interface DisciplinePerk { value: number; description: string; unlocksEffects?: string[]; + unlocksRecipes?: string[]; bonus?: PerkBonus; /** * For capped perks: maximum number of tiers. If unset, capped perks