From 0e1e50621394cf20a131400b684d064f550b061e Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Sun, 7 Jun 2026 23:15:55 +0200 Subject: [PATCH] fix: apply Crafting Efficiency cost reduction to all fabrication paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getCraftingCostReduction() and applyCostReduction() helpers in crafting-fabricator.ts - Apply cost reduction in deductMaterials() and checkFabricatorCosts() - Apply cost reduction in startFabricatorCrafting() and cancelEquipmentCrafting() pipeline - Update canCraftRecipe() in fabricator-recipes.ts to accept costReduction param - Update FabricatorSubTab and MaterialRecipeCard UIs to display discounted costs - Spec formula: actualCost = ceil(baseCost × (1 - craftingCostReduction / 100)) Fixes #316 --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- .../tabs/CraftingTab/FabricatorSubTab.tsx | 24 +++++++++++--- .../tabs/CraftingTab/MaterialRecipeCard.tsx | 15 ++++++--- src/lib/game/crafting-fabricator.ts | 32 +++++++++++++++++-- src/lib/game/data/fabricator-recipes.ts | 7 +++- .../stores/pipelines/equipment-crafting.ts | 11 +++++-- 7 files changed, 77 insertions(+), 16 deletions(-) diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 0fa1f05..660c9b4 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-06-07T16:09:18.340Z +Generated: 2026-06-07T21:06:17.789Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index b56b0a7..45b5358 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-07T16:09:16.437Z", + "generated": "2026-06-07T21:06:15.829Z", "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 880de14..b93b723 100644 --- a/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx +++ b/src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx @@ -15,6 +15,7 @@ import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops'; import { useCraftingStore, useManaStore } from '@/lib/game/stores'; import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes'; import { DebugName } from '@/components/game/debug/debug-context'; +import { getCraftingCostReduction, applyCostReduction } from '@/lib/game/crafting-fabricator'; const BRANCH_RECIPE_IDS = new Set([ 'oakStaff', 'arcanistStaff', 'battlestaff', 'arcanistCirclet', 'arcanistRobe', @@ -40,6 +41,7 @@ function RecipeCard({ elementalMana, onCraft, isCrafting, + costReduction, }: { recipe: FabricatorRecipe; materials: Record; @@ -47,6 +49,7 @@ function RecipeCard({ elementalMana: Record; onCraft: (recipe: FabricatorRecipe) => void; isCrafting: boolean; + costReduction: number; }) { const pool = recipe.manaType === 'raw' ? rawMana @@ -56,6 +59,7 @@ function RecipeCard({ materials, pool, recipe.manaType, + costReduction, ); const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity]; @@ -82,17 +86,26 @@ function RecipeCard({
-
Materials:
- {Object.entries(recipe.materials).map(([matId, amount]) => { +
+ Materials: + {costReduction > 0 && ( + -{costReduction}% cost + )} +
+ {Object.entries(recipe.materials).map(([matId, rawAmount]) => { + const reducedAmount = applyCostReduction(rawAmount, costReduction); const available = materials[matId] || 0; + const hasEnough = available >= reducedAmount; const matDrop = LOOT_DROPS[matId]; - const hasEnough = available >= amount; return (
{matDrop?.name ?? matId} - {available} / {amount} + {available} / {reducedAmount} + {costReduction > 0 && rawAmount !== reducedAmount && ( + (was {rawAmount}) + )}
); @@ -156,6 +169,7 @@ export function FabricatorSubTab() { }, [selectedManaType, branchFilter, unlockedRecipes]); const isCrafting = equipmentCraftingProgress !== null; + const costReduction = getCraftingCostReduction(); const materialRecipes = useMemo(() => MATERIAL_RECIPES, []); @@ -300,6 +314,7 @@ export function FabricatorSubTab() { elementalMana={elements} onCraft={handleCraft} isCrafting={isCrafting} + costReduction={costReduction} /> )) ) @@ -318,6 +333,7 @@ export function FabricatorSubTab() { rawMana={rawMana} elementalMana={elements} onCraft={handleCraft} + costReduction={costReduction} /> )) ) diff --git a/src/components/game/tabs/CraftingTab/MaterialRecipeCard.tsx b/src/components/game/tabs/CraftingTab/MaterialRecipeCard.tsx index 894c4ab..ac06f07 100644 --- a/src/components/game/tabs/CraftingTab/MaterialRecipeCard.tsx +++ b/src/components/game/tabs/CraftingTab/MaterialRecipeCard.tsx @@ -8,6 +8,7 @@ import { canCraftRecipe } from '@/lib/game/data/fabricator-recipes'; import { MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipe-types'; import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes'; import { DebugName } from '@/components/game/debug/debug-context'; +import { getCraftingCostReduction, applyCostReduction } from '@/lib/game/crafting-fabricator'; interface MaterialRecipeCardProps { recipe: FabricatorRecipe; @@ -15,6 +16,7 @@ interface MaterialRecipeCardProps { rawMana: number; elementalMana: Record; onCraft: (recipe: FabricatorRecipe) => void; + costReduction?: number; } export function MaterialRecipeCard({ @@ -23,11 +25,12 @@ export function MaterialRecipeCard({ rawMana, elementalMana, onCraft, + costReduction = 0, }: MaterialRecipeCardProps) { const pool = recipe.manaType === 'raw' ? rawMana : (elementalMana[recipe.manaType]?.current ?? 0); - const { canCraft } = canCraftRecipe(recipe, materials, pool, recipe.manaType); + const { canCraft } = canCraftRecipe(recipe, materials, pool, recipe.manaType, costReduction); const resultDrop = recipe.resultMaterial ? LOOT_DROPS[recipe.resultMaterial] : null; const resultRarity = resultDrop ? LOOT_RARITY_COLORS[resultDrop.rarity] : null; @@ -57,16 +60,20 @@ export function MaterialRecipeCard({ {Object.keys(recipe.materials).length > 0 && ( <>
Input Materials:
- {Object.entries(recipe.materials).map(([matId, amount]) => { + {Object.entries(recipe.materials).map(([matId, rawAmount]) => { + const reducedAmount = applyCostReduction(rawAmount, costReduction); const available = materials[matId] || 0; const matDrop = LOOT_DROPS[matId]; - const hasEnough = available >= amount; + const hasEnough = available >= reducedAmount; return (
{matDrop?.name ?? matId} - {available} / {amount} + {available} / {reducedAmount} + {costReduction > 0 && rawAmount !== reducedAmount && ( + (was {rawAmount}) + )}
); diff --git a/src/lib/game/crafting-fabricator.ts b/src/lib/game/crafting-fabricator.ts index 92aa6cd..7e379d2 100644 --- a/src/lib/game/crafting-fabricator.ts +++ b/src/lib/game/crafting-fabricator.ts @@ -6,6 +6,7 @@ import type { ElementState } from './types'; import type { FabricatorRecipe } from './data/fabricator-recipes'; import { FABRICATOR_RECIPES } from './data/fabricator-recipes'; import { useManaStore } from './stores/manaStore'; +import { computeDisciplineEffects } from './effects/discipline-effects'; // ─── Lookup ─────────────────────────────────────────────────────────────────── @@ -13,6 +14,27 @@ export function getFabricatorRecipe(recipeId: string): FabricatorRecipe | undefi return FABRICATOR_RECIPES.find(r => r.id === recipeId); } +// ─── Crafting Cost Reduction ────────────────────────────────────────────────── + +/** + * Returns the current crafting cost reduction percentage from discipline effects. + * Spec: actualCost = baseCost × (1 - craftingCostReduction / 100), capped at 75%. + */ +export function getCraftingCostReduction(): number { + const disciplineEffects = computeDisciplineEffects(); + const raw = disciplineEffects.bonuses.craftingCostReduction || 0; + return Math.min(75, raw); +} + +/** + * Apply cost reduction to a material amount, rounding up to ensure + * the player never pays more than the displayed reduced cost. + */ +export function applyCostReduction(baseAmount: number, costReduction: number): number { + if (costReduction <= 0) return baseAmount; + return Math.max(1, Math.ceil(baseAmount * (1 - costReduction / 100))); +} + // ─── Cost Check ─────────────────────────────────────────────────────────────── export interface FabricatorCostCheck { @@ -26,11 +48,14 @@ export function checkFabricatorCosts( materials: Record, rawMana: number, elements: Record, + costReduction?: number, ): FabricatorCostCheck { + const reduction = costReduction ?? getCraftingCostReduction(); const missingMaterials: Record = {}; let canCraft = true; - for (const [matId, required] of Object.entries(recipe.materials)) { + for (const [matId, rawRequired] of Object.entries(recipe.materials)) { + const required = applyCostReduction(rawRequired, reduction); const available = materials[matId] || 0; if (available < required) { missingMaterials[matId] = required - available; @@ -116,9 +141,12 @@ export function refundFabricatorMana( export function deductMaterials( recipe: FabricatorRecipe, materials: Record, + costReduction?: number, ): Record { + const reduction = costReduction ?? getCraftingCostReduction(); const newMaterials = { ...materials }; - for (const [matId, amount] of Object.entries(recipe.materials)) { + for (const [matId, rawAmount] of Object.entries(recipe.materials)) { + const amount = applyCostReduction(rawAmount, reduction); newMaterials[matId] = (newMaterials[matId] || 0) - amount; if (newMaterials[matId] <= 0) delete newMaterials[matId]; } diff --git a/src/lib/game/data/fabricator-recipes.ts b/src/lib/game/data/fabricator-recipes.ts index ecb7043..0c7d881 100644 --- a/src/lib/game/data/fabricator-recipes.ts +++ b/src/lib/game/data/fabricator-recipes.ts @@ -236,11 +236,16 @@ export function canCraftRecipe( materials: Record, manaAmount: number, manaType?: string, + costReduction?: number, ): { canCraft: boolean; missingMaterials: Record; missingMana: number } { + const reduction = costReduction ?? 0; const missingMaterials: Record = {}; let canCraft = true; - for (const [matId, required] of Object.entries(recipe.materials)) { + for (const [matId, rawRequired] of Object.entries(recipe.materials)) { + const required = reduction > 0 + ? Math.max(1, Math.ceil(rawRequired * (1 - reduction / 100))) + : rawRequired; const available = materials[matId] || 0; if (available < required) { missingMaterials[matId] = required - available; diff --git a/src/lib/game/stores/pipelines/equipment-crafting.ts b/src/lib/game/stores/pipelines/equipment-crafting.ts index 49ce2f8..6a11e29 100644 --- a/src/lib/game/stores/pipelines/equipment-crafting.ts +++ b/src/lib/game/stores/pipelines/equipment-crafting.ts @@ -10,6 +10,8 @@ import { deductMaterials, makeFabricatorProgress, refundFabricatorMana, + getCraftingCostReduction, + applyCostReduction, } from '../../crafting-fabricator'; import { useManaStore } from '../manaStore'; import { useCombatStore } from '../combatStore'; @@ -72,11 +74,13 @@ export function cancelEquipmentCrafting(get: GetFn, set: SetFn): void { const refunded = refundFabricatorMana(recipe, manaRefund, rawMana, elements); useManaStore.setState({ rawMana: refunded.rawMana, elements: refunded.elements }); - // Refund materials + // Refund materials — use reduced amounts (what player actually paid) + const reduction = getCraftingCostReduction(); const currentMaterials = get().lootInventory.materials; const refundedMaterials = { ...currentMaterials }; - for (const [matId, amount] of Object.entries(recipe.materials)) { - const refundAmount = Math.floor(amount * remainingFraction); + for (const [matId, rawAmount] of Object.entries(recipe.materials)) { + const reducedAmount = applyCostReduction(rawAmount, reduction); + const refundAmount = Math.floor(reducedAmount * remainingFraction); if (refundAmount > 0) { refundedMaterials[matId] = (refundedMaterials[matId] || 0) + refundAmount; } @@ -132,6 +136,7 @@ export function startFabricatorCrafting(recipeId: string, get: GetFn, set: SetFn const deducted = deductFabricatorMana(recipe, rawMana, elements); if (!deducted) return false; + // deductMaterials already applies cost reduction internally via getCraftingCostReduction() const newMaterials = deductMaterials(recipe, state.lootInventory.materials); const progress = makeFabricatorProgress(recipeId, recipe.equipmentTypeId, recipe.craftTime, recipe.manaCost);