Files
Mana-Loop/src/lib/game/crafting-slice.ts
T
Refactoring Agent c9ae2576f4
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m51s
fix: SLOT_NAMES export and refactor: split crafting-slice.ts into modules
2026-05-02 10:59:36 +02:00

379 lines
15 KiB
TypeScript
Executable File

// ─── Crafting Store Slice ─────────────────────────────────────────────────────
// Core slice logic for equipment and enchantment system. Extracted logic lives
// in focused modules: crafting-utils, crafting-design, crafting-prep,
// crafting-apply, crafting-equipment, crafting-loot, crafting-attunements.
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES, type EquipmentSlot } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import type { ComputedEffects } from './upgrade-effects.types';
// ─── Crafting Modules ───────────────────────────────────────────────────────
import * as CraftingUtils from './crafting-utils';
import * as CraftingDesign from './crafting-design';
import * as CraftingPrep from './crafting-prep';
import * as CraftingApply from './crafting-apply';
import * as CraftingEquipment from './crafting-equipment';
import * as CraftingLoot from './crafting-loot';
import * as CraftingAttunements from './crafting-attunements';
import * as CraftingActions from './crafting-actions';
// ─── Initial Equipment Setup ─────────────────────────────────────────────────
export function createStartingEquipment() {
const staffId = CraftingUtils.generateInstanceId();
const staffInstance: EquipmentInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
tags: [],
};
const shirtId = CraftingUtils.generateInstanceId();
const shirtInstance: EquipmentInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
tags: [],
};
const shoesId = CraftingUtils.generateInstanceId();
const shoesInstance: EquipmentInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
tags: [],
};
return {
equippedInstances: {
mainHand: staffId,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
},
equipmentInstances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
},
};
}
// ─── Crafting Actions Interface ─────────────────────────────────────────────
export type CraftingActions = {
createEquipmentInstance: (typeId: string) => string | null;
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
unequipItem: (slot: EquipmentSlot) => void;
deleteEquipmentInstance: (instanceId: string) => void;
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
cancelDesign: () => void;
saveDesign: (design: EnchantmentDesign) => void;
deleteDesign: (designId: string) => void;
startPreparing: (equipmentInstanceId: string) => boolean;
cancelPreparation: () => void;
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
pauseApplication: () => void;
resumeApplication: () => void;
cancelApplication: () => void;
disenchantEquipment: (instanceId: string) => void;
startCraftingEquipment: (blueprintId: string) => boolean;
cancelEquipmentCrafting: () => void;
deleteMaterial: (materialId: string, amount: number) => void;
getEquipmentSpells: () => string[];
getEquipmentEffects: () => Record<string, number>;
getAvailableCapacity: (instanceId: string) => number;
};
// ─── Crafting Store Slice ───────────────────────────────────────────────────
export function createCraftingSlice(
set: (fn: (state: GameState) => Partial<GameState>) => void,
get: () => GameState & CraftingActions
): CraftingActions {
return {
createEquipmentInstance: (typeId) => CraftingActions.createEquipmentInstance(typeId, set),
equipItem: (instanceId, slot) => CraftingActions.equipItem(instanceId, slot, get, set),
unequipItem: (slot) => CraftingActions.unequipItem(slot, set),
deleteEquipmentInstance: (instanceId) => CraftingActions.deleteEquipmentInstance(instanceId, get, set),
startDesigningEnchantment: (name, equipmentTypeId, effects) =>
CraftingActions.startDesigningEnchantment(name, equipmentTypeId, effects, get, set),
cancelDesign: () => CraftingActions.cancelDesign(get, set),
saveDesign: (design) => CraftingActions.saveDesign(design, get, set),
deleteDesign: (designId) => CraftingActions.deleteDesign(designId, set),
startPreparing: (equipmentInstanceId) => CraftingActions.startPreparing(equipmentInstanceId, get, set),
cancelPreparation: () => CraftingActions.cancelPreparation(set),
startApplying: (equipmentInstanceId, designId) =>
CraftingActions.startApplying(equipmentInstanceId, designId, get, set),
pauseApplication: () => CraftingActions.pauseApplication(get, set),
resumeApplication: () => CraftingActions.resumeApplication(get, set),
cancelApplication: () => CraftingActions.cancelApplication(set),
disenchantEquipment: (instanceId) => CraftingActions.disenchantEquipment(instanceId, get, set),
startCraftingEquipment: (blueprintId) => CraftingActions.startCraftingEquipment(blueprintId, get, set),
cancelEquipmentCrafting: () => CraftingActions.cancelEquipmentCrafting(get, set),
deleteMaterial: (materialId, amount) => CraftingActions.deleteMaterial(materialId, amount, get, set),
getEquipmentSpells: () => CraftingActions.getEquipmentSpells(get),
getEquipmentEffects: () => CraftingActions.getEquipmentEffects(get),
getAvailableCapacity: (instanceId) => CraftingActions.getAvailableCapacity(instanceId, get),
};
}
// ─── Tick Processing for Crafting ────────────────────────────────────────────
export function processCraftingTick(
state: GameState,
effects: { rawMana: number; log: string[] }
): Partial<GameState> {
const { rawMana, log } = effects;
let updates: Partial<GameState> = {};
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
// Process design progress (slot 1)
if (state.currentAction === 'design' && state.designProgress) {
const designResult = CraftingDesign.calculateDesignProgress(
state.designProgress.progress,
state.designProgress.required,
computedEffects,
false
);
if (designResult.isComplete) {
const completedDesign = CraftingDesign.createCompletedDesignFromProgress(
{
designId: state.designProgress.designId,
name: state.designProgress.name,
equipmentType: state.designProgress.equipmentType,
effects: state.designProgress.effects,
required: state.designProgress.required,
},
((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05
);
updates = {
...updates,
designProgress: null,
currentAction: 'meditate' as const,
enchantmentDesigns: [...state.enchantmentDesigns, completedDesign],
log: [`✅ Enchantment design "${completedDesign.name}" complete!`, ...log],
};
} else {
updates = {
...updates,
designProgress: { ...state.designProgress, progress: designResult.progress },
};
}
}
// Process second design progress (slot 2)
if (state.designProgress2) {
const designResult2 = CraftingDesign.calculateSecondDesignProgress(
state.designProgress2.progress,
state.designProgress2.required,
computedEffects,
false
);
if (designResult2.isComplete) {
const completedDesign = CraftingDesign.createCompletedDesignFromProgress(
{
designId: state.designProgress2.designId,
name: state.designProgress2.name,
equipmentType: state.designProgress2.equipmentType,
effects: state.designProgress2.effects,
required: state.designProgress2.required,
},
((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05
);
const shouldTransitionToMeditate = !state.designProgress;
updates = {
...updates,
designProgress2: null,
currentAction: shouldTransitionToMeditate ? 'meditate' as const : state.currentAction,
enchantmentDesigns: [...state.enchantmentDesigns, completedDesign],
log: [`✅ Enchantment design "${completedDesign.name}" complete! (2nd slot)`, ...log],
};
} else {
updates = {
...updates,
designProgress2: { ...state.designProgress2, progress: designResult2.progress },
};
}
}
// Process preparation progress
if (state.currentAction === 'prepare' && state.preparationProgress) {
const instance = state.equipmentInstances[state.preparationProgress.equipmentInstanceId];
const manaPerTick = instance ? CraftingPrep.getPreparationManaCostForTick(instance) : 0;
if (rawMana >= manaPerTick) {
const tickResult = CraftingPrep.calculatePreparationTick(
state.preparationProgress.progress,
state.preparationProgress.required,
manaPerTick
);
if (tickResult.isComplete) {
if (instance) {
const completeResult = CraftingPrep.completePreparation(instance, state.preparationProgress.manaCostPaid);
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed + completeResult.manaRecovered,
preparationProgress: null,
currentAction: 'meditate' as const,
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: completeResult.updatedInstance,
},
log: [completeResult.logMessage, ...log],
};
} else {
updates = {
...updates,
preparationProgress: null,
currentAction: 'meditate' as const,
rawMana: rawMana - tickResult.manaConsumed,
log: ['✅ Preparation complete!', ...log],
};
}
} else {
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
preparationProgress: {
...state.preparationProgress,
progress: tickResult.progress,
manaCostPaid: tickResult.manaCostPaid,
},
};
}
}
}
// Process application progress
if (state.currentAction === 'enchant' && state.applicationProgress && !state.applicationProgress.paused) {
const app = state.applicationProgress;
const manaPerTick = CraftingApply.getApplicationManaCostForTick(app.manaPerHour);
if (rawMana >= manaPerTick) {
const tickResult = CraftingApply.calculateApplicationTick(
app.progress,
app.required,
app.manaSpent,
manaPerTick,
computedEffects
);
if (tickResult.isComplete) {
const instance = state.equipmentInstances[app.equipmentInstanceId];
const design = state.enchantmentDesigns.find(d => d.id === app.designId);
if (instance && design) {
const applyResult = CraftingApply.applyEnchantments(instance, design, computedEffects);
const xpGain = CraftingAttunements.gainEnchantingXP(state.attunements, applyResult.xpGained);
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
applicationProgress: null,
currentAction: 'meditate' as const,
attunements: {
...state.attunements,
enchanter: xpGain.attunements.enchanter,
},
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: applyResult.updatedInstance,
},
log: [applyResult.logMessage, ...log],
};
}
} else {
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
applicationProgress: { ...app, progress: tickResult.progress, manaSpent: tickResult.manaSpent },
};
}
}
}
// Process equipment crafting progress
if (state.currentAction === 'craft' && state.equipmentCraftingProgress) {
const craft = state.equipmentCraftingProgress;
const tickResult = CraftingEquipment.calculateCraftingTick(craft.progress, craft.required);
if (tickResult.isComplete) {
const recipe = CraftingEquipment.getRecipe(craft.blueprintId);
if (recipe) {
const craftResult = CraftingEquipment.completeEquipmentCrafting(craft.blueprintId, recipe);
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate' as const,
equipmentInstances: {
...state.equipmentInstances,
[craftResult.instanceId]: craftResult.instance,
},
totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1,
log: [craftResult.logMessage, ...log],
};
} else {
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate' as const,
log: ['⚠️ Crafting failed - invalid recipe!', ...log],
};
}
} else {
updates = {
...updates,
equipmentCraftingProgress: { ...craft, progress: tickResult.progress },
};
}
}
return updates;
}
// ─── Export helper to get equipment instance spells ─────────────────────────
export function getSpellsFromEquipment(instances: Record<string, EquipmentInstance>, equippedIds: (string | null)[]): string[] {
const spells: string[] = [];
for (const id of equippedIds) {
if (!id) continue;
const instance = instances[id];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
}
}
}
return [...new Set(spells)];
}