Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 33s
- Add missing EquipmentCraftingProgress interface to types.ts - Fix CraftingTab to accept store prop like other tabs - Update attunement XP calculation: 1 XP per 10 capacity used (min 1) - Change XP requirements: 1000 for lv2, 2500 for lv3, doubling thereafter - Grant Enchanter XP when enchantments are applied - Add equipmentCraftingProgress to GameState and persist config
828 lines
26 KiB
TypeScript
Executable File
828 lines
26 KiB
TypeScript
Executable File
// ─── Crafting Store Slice ─────────────────────────────────────────────────────────
|
|
// Handles equipment and enchantment system: design, prepare, apply stages
|
|
|
|
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
|
|
import { EQUIPMENT_TYPES, type EquipmentCategory } 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';
|
|
|
|
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
|
|
|
function generateInstanceId(): string {
|
|
return `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
// Get equipment category from type
|
|
function getEquipmentCategory(typeId: string): EquipmentCategory | null {
|
|
const type = EQUIPMENT_TYPES[typeId];
|
|
return type?.category || null;
|
|
}
|
|
|
|
// Calculate total capacity cost for a design
|
|
function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number {
|
|
return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0);
|
|
}
|
|
|
|
// Calculate design time based on number and complexity of effects
|
|
function calculateDesignTime(effects: DesignEffect[]): number {
|
|
// Base 1 hour + 0.5 hours per effect stack
|
|
let time = 1;
|
|
for (const eff of effects) {
|
|
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
|
|
if (effectDef) {
|
|
time += 0.5 * eff.stacks;
|
|
}
|
|
}
|
|
return time;
|
|
}
|
|
|
|
// Calculate preparation time based on equipment capacity
|
|
function calculatePrepTime(equipmentCapacity: number): number {
|
|
// Base 2 hours + 1 hour per 50 capacity
|
|
return 2 + Math.floor(equipmentCapacity / 50);
|
|
}
|
|
|
|
// Calculate preparation mana cost
|
|
function calculatePrepManaCost(equipmentCapacity: number): number {
|
|
// 10 mana per capacity point
|
|
return equipmentCapacity * 10;
|
|
}
|
|
|
|
// Calculate application time based on design complexity
|
|
function calculateApplicationTime(design: EnchantmentDesign): number {
|
|
// 2 hours base + 1 hour per effect stack
|
|
return 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0);
|
|
}
|
|
|
|
// Calculate application mana cost per hour
|
|
function calculateApplicationManaPerHour(design: EnchantmentDesign): number {
|
|
// 20 mana per hour base + 5 per effect stack
|
|
return 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0);
|
|
}
|
|
|
|
// ─── Crafting Actions Interface ───────────────────────────────────────────────
|
|
|
|
export interface CraftingActions {
|
|
// Equipment management
|
|
createEquipmentInstance: (typeId: string) => string | null;
|
|
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
|
|
unequipItem: (slot: EquipmentSlot) => void;
|
|
deleteEquipmentInstance: (instanceId: string) => void;
|
|
|
|
// Enchantment design
|
|
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
|
|
cancelDesign: () => void;
|
|
saveDesign: (design: EnchantmentDesign) => void;
|
|
deleteDesign: (designId: string) => void;
|
|
|
|
// Enchantment preparation
|
|
startPreparing: (equipmentInstanceId: string) => boolean;
|
|
cancelPreparation: () => void;
|
|
|
|
// Enchantment application
|
|
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
|
|
pauseApplication: () => void;
|
|
resumeApplication: () => void;
|
|
cancelApplication: () => void;
|
|
|
|
// Disenchanting
|
|
disenchantEquipment: (instanceId: string) => void;
|
|
|
|
// Equipment Crafting (from blueprints)
|
|
startCraftingEquipment: (blueprintId: string) => boolean;
|
|
cancelEquipmentCrafting: () => void;
|
|
deleteMaterial: (materialId: string, amount: number) => void;
|
|
|
|
// Computed getters
|
|
getEquipmentSpells: () => string[];
|
|
getEquipmentEffects: () => Record<string, number>;
|
|
getAvailableCapacity: (instanceId: string) => number;
|
|
}
|
|
|
|
// ─── Initial Equipment Setup ───────────────────────────────────────────────────
|
|
|
|
export function createStartingEquipment(): {
|
|
equippedInstances: Record<string, string | null>;
|
|
equipmentInstances: Record<string, EquipmentInstance>;
|
|
} {
|
|
// Create starting staff with Mana Bolt enchantment
|
|
const staffId = 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,
|
|
};
|
|
|
|
// Create starting clothes
|
|
const shirtId = generateInstanceId();
|
|
const shirtInstance: EquipmentInstance = {
|
|
instanceId: shirtId,
|
|
typeId: 'civilianShirt',
|
|
name: 'Civilian Shirt',
|
|
enchantments: [],
|
|
usedCapacity: 0,
|
|
totalCapacity: 30,
|
|
rarity: 'common',
|
|
quality: 100,
|
|
};
|
|
|
|
const shoesId = generateInstanceId();
|
|
const shoesInstance: EquipmentInstance = {
|
|
instanceId: shoesId,
|
|
typeId: 'civilianShoes',
|
|
name: 'Civilian Shoes',
|
|
enchantments: [],
|
|
usedCapacity: 0,
|
|
totalCapacity: 15,
|
|
rarity: 'common',
|
|
quality: 100,
|
|
};
|
|
|
|
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 Store Extensions ─────────────────────────────────────────────────
|
|
|
|
export function createCraftingSlice(
|
|
set: (fn: (state: GameState) => Partial<GameState>) => void,
|
|
get: () => GameState & CraftingActions
|
|
): CraftingActions {
|
|
return {
|
|
// ─── Equipment Management ─────────────────────────────────────────────────
|
|
|
|
createEquipmentInstance: (typeId: string) => {
|
|
const type = EQUIPMENT_TYPES[typeId];
|
|
if (!type) return null;
|
|
|
|
const instanceId = generateInstanceId();
|
|
const instance: EquipmentInstance = {
|
|
instanceId,
|
|
typeId,
|
|
name: type.name,
|
|
enchantments: [],
|
|
usedCapacity: 0,
|
|
totalCapacity: type.baseCapacity,
|
|
rarity: 'common',
|
|
quality: 100,
|
|
};
|
|
|
|
set((state) => ({
|
|
equipmentInstances: {
|
|
...state.equipmentInstances,
|
|
[instanceId]: instance,
|
|
},
|
|
}));
|
|
|
|
return instanceId;
|
|
},
|
|
|
|
equipItem: (instanceId: string, slot: EquipmentSlot) => {
|
|
const state = get();
|
|
const instance = state.equipmentInstances[instanceId];
|
|
if (!instance) return false;
|
|
|
|
const type = EQUIPMENT_TYPES[instance.typeId];
|
|
if (!type) return false;
|
|
|
|
// Check if equipment can go in this slot
|
|
const validSlots = type.category === 'accessory'
|
|
? ['accessory1', 'accessory2']
|
|
: [type.slot];
|
|
|
|
if (!validSlots.includes(slot)) return false;
|
|
|
|
// Check if slot is occupied
|
|
const currentEquipped = state.equippedInstances[slot];
|
|
if (currentEquipped === instanceId) return true; // Already equipped here
|
|
|
|
// If this item is equipped elsewhere, unequip it first
|
|
let newEquipped = { ...state.equippedInstances };
|
|
for (const [s, id] of Object.entries(newEquipped)) {
|
|
if (id === instanceId) {
|
|
newEquipped[s as EquipmentSlot] = null;
|
|
}
|
|
}
|
|
|
|
// Equip to new slot
|
|
newEquipped[slot] = instanceId;
|
|
|
|
set(() => ({ equippedInstances: newEquipped }));
|
|
return true;
|
|
},
|
|
|
|
unequipItem: (slot: EquipmentSlot) => {
|
|
set((state) => ({
|
|
equippedInstances: {
|
|
...state.equippedInstances,
|
|
[slot]: null,
|
|
},
|
|
}));
|
|
},
|
|
|
|
deleteEquipmentInstance: (instanceId: string) => {
|
|
set((state) => {
|
|
// First unequip if equipped
|
|
let newEquipped = { ...state.equippedInstances };
|
|
for (const [slot, id] of Object.entries(newEquipped)) {
|
|
if (id === instanceId) {
|
|
newEquipped[slot as EquipmentSlot] = null;
|
|
}
|
|
}
|
|
|
|
// Remove from instances
|
|
const newInstances = { ...state.equipmentInstances };
|
|
delete newInstances[instanceId];
|
|
|
|
return {
|
|
equippedInstances: newEquipped,
|
|
equipmentInstances: newInstances,
|
|
};
|
|
});
|
|
},
|
|
|
|
// ─── Enchantment Design ─────────────────────────────────────────────────
|
|
|
|
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => {
|
|
const state = get();
|
|
|
|
// Check if player has enchanting skill
|
|
const enchantingLevel = state.skills.enchanting || 0;
|
|
if (enchantingLevel < 1) return false;
|
|
|
|
// Validate effects for equipment category
|
|
const category = getEquipmentCategory(equipmentTypeId);
|
|
if (!category) return false;
|
|
|
|
for (const eff of effects) {
|
|
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
|
|
if (!effectDef) return false;
|
|
if (!effectDef.allowedEquipmentCategories.includes(category)) return false;
|
|
if (eff.stacks > effectDef.maxStacks) return false;
|
|
}
|
|
|
|
// Calculate capacity cost
|
|
const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05;
|
|
const totalCapacityCost = calculateDesignCapacityCost(effects, efficiencyBonus);
|
|
|
|
// Create design
|
|
const designTime = calculateDesignTime(effects);
|
|
const design: EnchantmentDesign = {
|
|
id: `design_${Date.now()}`,
|
|
name,
|
|
equipmentType: equipmentTypeId,
|
|
effects,
|
|
totalCapacityUsed: totalCapacityCost,
|
|
designTime,
|
|
created: Date.now(),
|
|
};
|
|
|
|
set(() => ({
|
|
currentAction: 'design',
|
|
designProgress: {
|
|
designId: design.id,
|
|
progress: 0,
|
|
required: designTime,
|
|
},
|
|
}));
|
|
|
|
return true;
|
|
},
|
|
|
|
cancelDesign: () => {
|
|
set(() => ({
|
|
currentAction: 'meditate',
|
|
designProgress: null,
|
|
}));
|
|
},
|
|
|
|
saveDesign: (design: EnchantmentDesign) => {
|
|
set((state) => ({
|
|
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
|
designProgress: null,
|
|
currentAction: 'meditate',
|
|
}));
|
|
},
|
|
|
|
deleteDesign: (designId: string) => {
|
|
set((state) => ({
|
|
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
|
}));
|
|
},
|
|
|
|
// ─── Enchantment Preparation ─────────────────────────────────────────────
|
|
|
|
startPreparing: (equipmentInstanceId: string) => {
|
|
const state = get();
|
|
const instance = state.equipmentInstances[equipmentInstanceId];
|
|
if (!instance) return false;
|
|
|
|
const prepTime = calculatePrepTime(instance.totalCapacity);
|
|
const manaCost = calculatePrepManaCost(instance.totalCapacity);
|
|
|
|
if (state.rawMana < manaCost) return false;
|
|
|
|
set(() => ({
|
|
currentAction: 'prepare',
|
|
preparationProgress: {
|
|
equipmentInstanceId,
|
|
progress: 0,
|
|
required: prepTime,
|
|
manaCostPaid: 0,
|
|
},
|
|
}));
|
|
|
|
return true;
|
|
},
|
|
|
|
cancelPreparation: () => {
|
|
set(() => ({
|
|
currentAction: 'meditate',
|
|
preparationProgress: null,
|
|
}));
|
|
},
|
|
|
|
// ─── Enchantment Application ─────────────────────────────────────────────
|
|
|
|
startApplying: (equipmentInstanceId: string, designId: string) => {
|
|
const state = get();
|
|
const instance = state.equipmentInstances[equipmentInstanceId];
|
|
const design = state.enchantmentDesigns.find(d => d.id === designId);
|
|
|
|
if (!instance || !design) return false;
|
|
|
|
// Check capacity
|
|
if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) {
|
|
return false;
|
|
}
|
|
|
|
const applicationTime = calculateApplicationTime(design);
|
|
const manaPerHour = calculateApplicationManaPerHour(design);
|
|
|
|
set(() => ({
|
|
currentAction: 'enchant',
|
|
applicationProgress: {
|
|
equipmentInstanceId,
|
|
designId,
|
|
progress: 0,
|
|
required: applicationTime,
|
|
manaPerHour,
|
|
paused: false,
|
|
manaSpent: 0,
|
|
},
|
|
}));
|
|
|
|
return true;
|
|
},
|
|
|
|
pauseApplication: () => {
|
|
set((state) => {
|
|
if (!state.applicationProgress) return {};
|
|
return {
|
|
applicationProgress: {
|
|
...state.applicationProgress,
|
|
paused: true,
|
|
},
|
|
};
|
|
});
|
|
},
|
|
|
|
resumeApplication: () => {
|
|
set((state) => {
|
|
if (!state.applicationProgress) return {};
|
|
return {
|
|
applicationProgress: {
|
|
...state.applicationProgress,
|
|
paused: false,
|
|
},
|
|
};
|
|
});
|
|
},
|
|
|
|
cancelApplication: () => {
|
|
set(() => ({
|
|
currentAction: 'meditate',
|
|
applicationProgress: null,
|
|
}));
|
|
},
|
|
|
|
// ─── Disenchanting ─────────────────────────────────────────────────────────
|
|
|
|
disenchantEquipment: (instanceId: string) => {
|
|
const state = get();
|
|
const instance = state.equipmentInstances[instanceId];
|
|
if (!instance || instance.enchantments.length === 0) return;
|
|
|
|
const disenchantLevel = state.skills.disenchanting || 0;
|
|
const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level
|
|
|
|
let totalRecovered = 0;
|
|
for (const ench of instance.enchantments) {
|
|
totalRecovered += Math.floor(ench.actualCost * recoveryRate);
|
|
}
|
|
|
|
set((state) => ({
|
|
rawMana: state.rawMana + totalRecovered,
|
|
equipmentInstances: {
|
|
...state.equipmentInstances,
|
|
[instanceId]: {
|
|
...instance,
|
|
enchantments: [],
|
|
usedCapacity: 0,
|
|
},
|
|
},
|
|
log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)],
|
|
}));
|
|
},
|
|
|
|
// ─── Computed Getters ─────────────────────────────────────────────────────
|
|
|
|
getEquipmentSpells: () => {
|
|
const state = get();
|
|
const spells: string[] = [];
|
|
|
|
for (const instanceId of Object.values(state.equippedInstances)) {
|
|
if (!instanceId) continue;
|
|
const instance = state.equipmentInstances[instanceId];
|
|
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)]; // Remove duplicates
|
|
},
|
|
|
|
getEquipmentEffects: () => {
|
|
const state = get();
|
|
const effects: Record<string, number> = {};
|
|
|
|
for (const instanceId of Object.values(state.equippedInstances)) {
|
|
if (!instanceId) continue;
|
|
const instance = state.equipmentInstances[instanceId];
|
|
if (!instance) continue;
|
|
|
|
for (const ench of instance.enchantments) {
|
|
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
|
if (!effectDef) continue;
|
|
|
|
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) {
|
|
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks;
|
|
}
|
|
}
|
|
}
|
|
|
|
return effects;
|
|
},
|
|
|
|
getAvailableCapacity: (instanceId: string) => {
|
|
const state = get();
|
|
const instance = state.equipmentInstances[instanceId];
|
|
if (!instance) return 0;
|
|
return instance.totalCapacity - instance.usedCapacity;
|
|
},
|
|
|
|
// ─── Equipment Crafting (from Blueprints) ───────────────────────────────────
|
|
|
|
startCraftingEquipment: (blueprintId: string) => {
|
|
const state = get();
|
|
const recipe = CRAFTING_RECIPES[blueprintId];
|
|
if (!recipe) return false;
|
|
|
|
// Check if player has the blueprint
|
|
if (!state.lootInventory.blueprints.includes(blueprintId)) return false;
|
|
|
|
// Check materials
|
|
const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
|
|
recipe,
|
|
state.lootInventory.materials,
|
|
state.rawMana
|
|
);
|
|
|
|
if (!canCraft) return false;
|
|
|
|
// Deduct materials
|
|
const newMaterials = { ...state.lootInventory.materials };
|
|
for (const [matId, amount] of Object.entries(recipe.materials)) {
|
|
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
|
|
if (newMaterials[matId] <= 0) {
|
|
delete newMaterials[matId];
|
|
}
|
|
}
|
|
|
|
// Start crafting progress
|
|
set((state) => ({
|
|
lootInventory: {
|
|
...state.lootInventory,
|
|
materials: newMaterials,
|
|
},
|
|
rawMana: state.rawMana - recipe.manaCost,
|
|
currentAction: 'craft',
|
|
equipmentCraftingProgress: {
|
|
blueprintId,
|
|
equipmentTypeId: recipe.equipmentTypeId,
|
|
progress: 0,
|
|
required: recipe.craftTime,
|
|
manaSpent: recipe.manaCost,
|
|
},
|
|
}));
|
|
|
|
return true;
|
|
},
|
|
|
|
cancelEquipmentCrafting: () => {
|
|
set((state) => {
|
|
const progress = state.equipmentCraftingProgress;
|
|
if (!progress) return {};
|
|
|
|
const recipe = CRAFTING_RECIPES[progress.blueprintId];
|
|
if (!recipe) return { currentAction: 'meditate', equipmentCraftingProgress: null };
|
|
|
|
// Refund 50% of mana
|
|
const manaRefund = Math.floor(progress.manaSpent * 0.5);
|
|
|
|
return {
|
|
currentAction: 'meditate',
|
|
equipmentCraftingProgress: null,
|
|
rawMana: state.rawMana + manaRefund,
|
|
log: [`🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.`, ...state.log.slice(0, 49)],
|
|
};
|
|
});
|
|
},
|
|
|
|
deleteMaterial: (materialId: string, amount: number) => {
|
|
set((state) => {
|
|
const currentAmount = state.lootInventory.materials[materialId] || 0;
|
|
const newAmount = Math.max(0, currentAmount - amount);
|
|
const newMaterials = { ...state.lootInventory.materials };
|
|
|
|
if (newAmount <= 0) {
|
|
delete newMaterials[materialId];
|
|
} else {
|
|
newMaterials[materialId] = newAmount;
|
|
}
|
|
|
|
const dropName = materialId; // Could look up in LOOT_DROPS for proper name
|
|
return {
|
|
lootInventory: {
|
|
...state.lootInventory,
|
|
materials: newMaterials,
|
|
},
|
|
log: [`🗑️ Deleted ${amount}x ${dropName}.`, ...state.log.slice(0, 49)],
|
|
};
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// ─── Tick Processing for Crafting ─────────────────────────────────────────────
|
|
|
|
export function processCraftingTick(
|
|
state: GameState,
|
|
effects: { rawMana: number; log: string[] }
|
|
): Partial<GameState> {
|
|
const { rawMana, log } = effects;
|
|
let updates: Partial<GameState> = {};
|
|
|
|
// Process design progress
|
|
if (state.currentAction === 'design' && state.designProgress) {
|
|
const progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK
|
|
if (progress >= state.designProgress.required) {
|
|
// Design complete - but we need the design data to save it
|
|
// This will be handled by the UI calling saveDesign
|
|
updates = {
|
|
...updates,
|
|
log: ['✅ Enchantment design complete!', ...log],
|
|
};
|
|
} else {
|
|
updates = {
|
|
...updates,
|
|
designProgress: {
|
|
...state.designProgress,
|
|
progress,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Process preparation progress
|
|
if (state.currentAction === 'prepare' && state.preparationProgress) {
|
|
const prep = state.preparationProgress;
|
|
const manaPerHour = calculatePrepManaCost(
|
|
state.equipmentInstances[prep.equipmentInstanceId]?.totalCapacity || 50
|
|
) / prep.required;
|
|
const manaCost = manaPerHour * 0.04; // HOURS_PER_TICK
|
|
|
|
if (rawMana >= manaCost) {
|
|
const progress = prep.progress + 0.04;
|
|
const manaCostPaid = prep.manaCostPaid + manaCost;
|
|
|
|
if (progress >= prep.required) {
|
|
updates = {
|
|
...updates,
|
|
rawMana: rawMana - manaCost,
|
|
preparationProgress: null,
|
|
currentAction: 'meditate',
|
|
log: ['✅ Equipment prepared for enchanting!', ...log],
|
|
};
|
|
} else {
|
|
updates = {
|
|
...updates,
|
|
rawMana: rawMana - manaCost,
|
|
preparationProgress: {
|
|
...prep,
|
|
progress,
|
|
manaCostPaid,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process application progress
|
|
if (state.currentAction === 'enchant' && state.applicationProgress && !state.applicationProgress.paused) {
|
|
const app = state.applicationProgress;
|
|
const manaCost = app.manaPerHour * 0.04; // HOURS_PER_TICK
|
|
|
|
if (rawMana >= manaCost) {
|
|
const progress = app.progress + 0.04;
|
|
const manaSpent = app.manaSpent + manaCost;
|
|
|
|
if (progress >= app.required) {
|
|
// Apply the enchantment!
|
|
const instance = state.equipmentInstances[app.equipmentInstanceId];
|
|
const design = state.enchantmentDesigns.find(d => d.id === app.designId);
|
|
|
|
if (instance && design) {
|
|
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => ({
|
|
effectId: eff.effectId,
|
|
stacks: eff.stacks,
|
|
actualCost: eff.capacityCost,
|
|
}));
|
|
|
|
// Calculate and grant attunement XP to enchanter
|
|
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
|
|
let newAttunements = state.attunements;
|
|
|
|
if (state.attunements.enchanter?.active && xpGained > 0) {
|
|
const enchanterState = state.attunements.enchanter;
|
|
let newXP = enchanterState.experience + xpGained;
|
|
let newLevel = enchanterState.level;
|
|
|
|
// Check for level ups
|
|
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
|
|
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
|
|
if (newXP >= xpNeeded) {
|
|
newXP -= xpNeeded;
|
|
newLevel++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
newAttunements = {
|
|
...state.attunements,
|
|
enchanter: {
|
|
...enchanterState,
|
|
level: newLevel,
|
|
experience: newXP,
|
|
},
|
|
};
|
|
}
|
|
|
|
updates = {
|
|
...updates,
|
|
rawMana: rawMana - manaCost,
|
|
applicationProgress: null,
|
|
currentAction: 'meditate',
|
|
attunements: newAttunements,
|
|
equipmentInstances: {
|
|
...state.equipmentInstances,
|
|
[app.equipmentInstanceId]: {
|
|
...instance,
|
|
enchantments: [...instance.enchantments, ...newEnchantments],
|
|
usedCapacity: instance.usedCapacity + design.totalCapacityUsed,
|
|
},
|
|
},
|
|
log: [`✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, ...log],
|
|
};
|
|
}
|
|
} else {
|
|
updates = {
|
|
...updates,
|
|
rawMana: rawMana - manaCost,
|
|
applicationProgress: {
|
|
...app,
|
|
progress,
|
|
manaSpent,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process equipment crafting progress
|
|
if (state.currentAction === 'craft' && state.equipmentCraftingProgress) {
|
|
const craft = state.equipmentCraftingProgress;
|
|
const progress = craft.progress + 0.04; // HOURS_PER_TICK
|
|
|
|
if (progress >= craft.required) {
|
|
// Crafting complete - create the equipment!
|
|
const recipe = CRAFTING_RECIPES[craft.blueprintId];
|
|
const equipType = recipe ? EQUIPMENT_TYPES[recipe.equipmentTypeId] : null;
|
|
|
|
if (recipe && equipType) {
|
|
const instanceId = generateInstanceId();
|
|
const newInstance: EquipmentInstance = {
|
|
instanceId,
|
|
typeId: recipe.equipmentTypeId,
|
|
name: recipe.name,
|
|
enchantments: [],
|
|
usedCapacity: 0,
|
|
totalCapacity: equipType.baseCapacity,
|
|
rarity: recipe.rarity,
|
|
quality: 100,
|
|
};
|
|
|
|
updates = {
|
|
...updates,
|
|
equipmentCraftingProgress: null,
|
|
currentAction: 'meditate',
|
|
equipmentInstances: {
|
|
...state.equipmentInstances,
|
|
[instanceId]: newInstance,
|
|
},
|
|
totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1,
|
|
log: [`🔨 Crafted ${recipe.name}!`, ...log],
|
|
};
|
|
} else {
|
|
updates = {
|
|
...updates,
|
|
equipmentCraftingProgress: null,
|
|
currentAction: 'meditate',
|
|
log: ['⚠️ Crafting failed - invalid recipe!', ...log],
|
|
};
|
|
}
|
|
} else {
|
|
updates = {
|
|
...updates,
|
|
equipmentCraftingProgress: {
|
|
...craft,
|
|
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)];
|
|
}
|