0e1e506213
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
- 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
206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
// ─── Fabricator Crafting Helpers ──────────────────────────────────────────────
|
||
// Separate file to avoid exceeding the 400-line limit in craftingStore.ts.
|
||
|
||
import type { EquipmentCraftingProgress } from './types';
|
||
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 ───────────────────────────────────────────────────────────────────
|
||
|
||
export function getFabricatorRecipe(recipeId: string): FabricatorRecipe | undefined {
|
||
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 {
|
||
canCraft: boolean;
|
||
missingMana: number;
|
||
missingMaterials: Record<string, number>;
|
||
}
|
||
|
||
export function checkFabricatorCosts(
|
||
recipe: FabricatorRecipe,
|
||
materials: Record<string, number>,
|
||
rawMana: number,
|
||
elements: Record<string, ElementState>,
|
||
costReduction?: number,
|
||
): FabricatorCostCheck {
|
||
const reduction = costReduction ?? getCraftingCostReduction();
|
||
const missingMaterials: Record<string, number> = {};
|
||
let canCraft = true;
|
||
|
||
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;
|
||
canCraft = false;
|
||
}
|
||
}
|
||
|
||
const manaAvailable = recipe.manaType === 'raw'
|
||
? rawMana
|
||
: (elements[recipe.manaType]?.current ?? 0);
|
||
const missingMana = Math.max(0, recipe.manaCost - manaAvailable);
|
||
if (missingMana > 0) canCraft = false;
|
||
|
||
return { canCraft, missingMana, missingMaterials };
|
||
}
|
||
|
||
// ─── Mana Deduction ───────────────────────────────────────────────────────────
|
||
|
||
export interface ManaDeduction {
|
||
rawMana: number;
|
||
elements: Record<string, ElementState>;
|
||
}
|
||
|
||
export function deductFabricatorMana(
|
||
recipe: FabricatorRecipe,
|
||
rawMana: number,
|
||
elements: Record<string, ElementState>,
|
||
): ManaDeduction | null {
|
||
if (recipe.manaType === 'raw') {
|
||
if (rawMana < recipe.manaCost) return null;
|
||
return { rawMana: rawMana - recipe.manaCost, elements };
|
||
}
|
||
|
||
const elemState = elements[recipe.manaType];
|
||
if (!elemState || !elemState.unlocked || elemState.current < recipe.manaCost) return null;
|
||
|
||
return {
|
||
rawMana,
|
||
elements: {
|
||
...elements,
|
||
[recipe.manaType]: {
|
||
...elements[recipe.manaType],
|
||
current: elements[recipe.manaType].current - recipe.manaCost,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
// ─── Mana Refund (on cancel) ──────────────────────────────────────────────────
|
||
|
||
export interface ManaRefund {
|
||
rawMana: number;
|
||
elements: Record<string, ElementState>;
|
||
}
|
||
|
||
export function refundFabricatorMana(
|
||
recipe: FabricatorRecipe,
|
||
refundAmount: number,
|
||
rawMana: number,
|
||
elements: Record<string, ElementState>,
|
||
): ManaRefund {
|
||
if (recipe.manaType === 'raw') {
|
||
return { rawMana: rawMana + refundAmount, elements };
|
||
}
|
||
|
||
return {
|
||
rawMana,
|
||
elements: {
|
||
...elements,
|
||
[recipe.manaType]: {
|
||
...elements[recipe.manaType],
|
||
current: Math.min(
|
||
elements[recipe.manaType].max,
|
||
elements[recipe.manaType].current + refundAmount,
|
||
),
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
// ─── Material Deduction ───────────────────────────────────────────────────────
|
||
|
||
export function deductMaterials(
|
||
recipe: FabricatorRecipe,
|
||
materials: Record<string, number>,
|
||
costReduction?: number,
|
||
): Record<string, number> {
|
||
const reduction = costReduction ?? getCraftingCostReduction();
|
||
const newMaterials = { ...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];
|
||
}
|
||
return newMaterials;
|
||
}
|
||
|
||
// ─── Progress Init ────────────────────────────────────────────────────────────
|
||
|
||
export function makeFabricatorProgress(
|
||
recipeId: string,
|
||
equipmentTypeId: string,
|
||
craftTime: number,
|
||
manaCost: number,
|
||
): EquipmentCraftingProgress {
|
||
return {
|
||
blueprintId: `fabricator-${recipeId}`,
|
||
equipmentTypeId,
|
||
progress: 0,
|
||
required: craftTime,
|
||
manaSpent: manaCost,
|
||
};
|
||
}
|
||
|
||
// ─── Material Crafting ──────────────────────────────────────────────────────
|
||
|
||
export interface CraftMaterialResult {
|
||
success: boolean;
|
||
newMaterials: Record<string, number>;
|
||
newRawMana: number;
|
||
newElements: Record<string, ElementState>;
|
||
logMessage: string;
|
||
}
|
||
|
||
export function executeMaterialCraft(
|
||
recipe: FabricatorRecipe,
|
||
materials: Record<string, number>,
|
||
): CraftMaterialResult | null {
|
||
if (recipe.recipeType !== 'material' || !recipe.resultMaterial || !recipe.resultAmount) return null;
|
||
|
||
const rawMana = useManaStore.getState().rawMana;
|
||
const elements = useManaStore.getState().elements;
|
||
|
||
const deducted = deductFabricatorMana(recipe, rawMana, elements);
|
||
if (!deducted) return null;
|
||
|
||
const newMaterials = deductMaterials(recipe, materials);
|
||
newMaterials[recipe.resultMaterial] = (newMaterials[recipe.resultMaterial] || 0) + recipe.resultAmount;
|
||
|
||
return {
|
||
success: true,
|
||
newMaterials,
|
||
newRawMana: deducted.rawMana,
|
||
newElements: deducted.elements,
|
||
logMessage: `✨ Crafted ${recipe.resultAmount}x ${recipe.name}!`,
|
||
};
|
||
}
|