379 lines
15 KiB
TypeScript
Executable File
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)];
|
|
}
|