fix: SLOT_NAMES export and refactor: split crafting-slice.ts into modules
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m51s

This commit is contained in:
Refactoring Agent
2026-05-02 10:59:36 +02:00
parent dc38445225
commit c9ae2576f4
14 changed files with 2213 additions and 942 deletions
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { EquipmentSlot } from '@/lib/game/data/equipment'; import { EquipmentSlot } from '@/lib/game/data/equipment';
import { SLOT_NAMES } from './EquipmentTab'; import { SLOT_NAMES } from '@/lib/game/data/equipment';
import type { GameStore, EquipmentInstance } from '@/lib/game/types'; import type { GameStore, EquipmentInstance } from '@/lib/game/types';
import { GameCard } from '@/components/ui/game-card'; import { GameCard } from '@/components/ui/game-card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
+1 -12
View File
@@ -4,6 +4,7 @@ import { useState, useMemo } from 'react';
import { import {
EQUIPMENT_TYPES, EQUIPMENT_TYPES,
EQUIPMENT_SLOTS, EQUIPMENT_SLOTS,
SLOT_NAMES,
getEquipmentBySlot, getEquipmentBySlot,
type EquipmentSlot, type EquipmentSlot,
type EquipmentType, type EquipmentType,
@@ -23,18 +24,6 @@ import { EnchantmentsPanel } from './EnchantmentsPanel';
import { useGameToast } from '@/components/game/GameToast'; import { useGameToast } from '@/components/game/GameToast';
import { ConfirmDialog } from '@/components/game/ConfirmDialog'; import { ConfirmDialog } from '@/components/game/ConfirmDialog';
// Slot display names
const SLOT_NAMES: Record<EquipmentSlot, string> = {
mainHand: 'Main Hand',
offHand: 'Off Hand',
head: 'Head',
body: 'Body',
hands: 'Hands',
feet: 'Feet',
accessory1: 'Accessory 1',
accessory2: 'Accessory 2',
};
// Rarity color mappings using design system tokens // Rarity color mappings using design system tokens
export const RARITY_BORDER_COLORS: Record<string, string> = { export const RARITY_BORDER_COLORS: Record<string, string> = {
common: 'border-[var(--text-muted)]', common: 'border-[var(--text-muted)]',
+513
View File
@@ -0,0 +1,513 @@
// ─── Crafting Action Implementations ──────────────────────────────────────────
// Action implementations for crafting-slice.ts. Extracted to keep main slice focused.
// These functions implement the CraftingActions interface defined in crafting-slice.ts
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES, type EquipmentSlot } from './data/equipment';
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
import { CRAFTING_RECIPES } from './data/crafting-recipes';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
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';
// ─── Equipment Management Actions ────────────────────────────────────────────
// Create equipment instance
export function createEquipmentInstance(
typeId: string,
set: (fn: (state: GameState) => Partial<GameState>) => void
): string | null {
const type = CraftingUtils.getEquipmentType(typeId);
if (!type) return null;
const instanceId = CraftingUtils.generateInstanceId();
const instance: EquipmentInstance = {
instanceId,
typeId,
name: type.name,
enchantments: [],
usedCapacity: 0,
totalCapacity: type.baseCapacity,
rarity: 'common',
quality: 100,
tags: [],
};
set((state) => ({
equipmentInstances: {
...state.equipmentInstances,
[instanceId]: instance,
},
}));
return instanceId;
}
// Equip item
export function equipItem(
instanceId: string,
slot: EquipmentSlot,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
): boolean {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance) return false;
if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances)) {
return false;
}
let newEquipped = { ...state.equippedInstances };
for (const [s, id] of Object.entries(newEquipped)) {
if (id === instanceId) {
newEquipped[s as EquipmentSlot] = null;
}
}
newEquipped[slot] = instanceId;
if (CraftingUtils.isTwoHanded(instance.typeId) && slot === 'mainHand') {
newEquipped.offHand = null;
}
set(() => ({ equippedInstances: newEquipped }));
return true;
}
// Unequip item
export function unequipItem(
slot: EquipmentSlot,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => ({
equippedInstances: {
...state.equippedInstances,
[slot]: null,
},
}));
}
// Delete equipment instance
export function deleteEquipmentInstance(
instanceId: string,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
const state = get();
let newEquipped = { ...state.equippedInstances };
for (const [slot, id] of Object.entries(newEquipped)) {
if (id === instanceId) {
newEquipped[slot as EquipmentSlot] = null;
}
}
const newInstances = { ...state.equipmentInstances };
delete newInstances[instanceId];
set(() => ({
equippedInstances: newEquipped,
equipmentInstances: newInstances,
}));
}
// ─── Enchantment Design Actions ────────────────────────────────────────────
export function startDesigningEnchantment(
name: string,
equipmentTypeId: string,
effects: DesignEffect[],
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
): boolean {
const state = get();
const enchantingLevel = state.skills.enchanting || 0;
const validation = CraftingDesign.validateDesignEffects(
effects,
equipmentTypeId,
enchantingLevel
);
if (!validation.valid) return false;
const equipType = CraftingUtils.getEquipmentType(equipmentTypeId);
if (!equipType) return false;
const efficiencyBonus = ((state.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0;
const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus);
if (totalCapacityCost > equipType.baseCapacity) {
return false;
}
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY);
let updates: any = {};
if (!state.designProgress) {
updates = {
currentAction: 'design' as const,
designProgress: {
designId: CraftingUtils.generateDesignId(),
progress: 0,
required: CraftingDesign.calculateDesignTime(effects),
name,
equipmentType: equipmentTypeId,
effects,
},
};
} else if (hasEnchantMastery && !state.designProgress2) {
updates = {
designProgress2: {
designId: CraftingUtils.generateDesignId(),
progress: 0,
required: CraftingDesign.calculateDesignTime(effects),
name,
equipmentType: equipmentTypeId,
effects,
},
};
} else {
return false;
}
set(() => updates);
return true;
}
export function cancelDesign(
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
const state = get();
if (state.designProgress2 && !state.designProgress) {
set(() => ({ designProgress2: null }));
} else {
set(() => ({
currentAction: 'meditate' as const,
designProgress: null,
}));
}
}
export function saveDesign(
design: EnchantmentDesign,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
const state = get();
if (state.designProgress2 && state.designProgress2.designId === design.id) {
set((state) => ({
enchantmentDesigns: [...state.enchantmentDesigns, design],
designProgress2: null,
}));
} else {
set((state) => ({
enchantmentDesigns: [...state.enchantmentDesigns, design],
designProgress: null,
currentAction: 'meditate' as const,
}));
}
}
export function deleteDesign(
designId: string,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => ({
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
}));
}
// ─── Enchantment Preparation Actions ────────────────────────────────────────
export function startPreparing(
equipmentInstanceId: string,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
): boolean {
const state = get();
const instance = state.equipmentInstances[equipmentInstanceId];
const validation = CraftingPrep.canPrepareEquipment(
instance,
instance?.tags || []
);
if (!validation.canPrepare) return false;
if (!instance) return false;
const costs = CraftingPrep.calculatePreparationCosts(instance.totalCapacity);
if (state.rawMana < costs.manaTotal) return false;
set(() => ({
currentAction: 'prepare' as const,
preparationProgress: CraftingPrep.initializePreparationProgress(
equipmentInstanceId,
instance.totalCapacity
),
}));
return true;
}
export function cancelPreparation(
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set(() => ({
currentAction: 'meditate' as const,
preparationProgress: null,
}));
}
// ─── Enchantment Application Actions ────────────────────────────────────────
export function startApplying(
equipmentInstanceId: string,
designId: string,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
): boolean {
const state = get();
const instance = state.equipmentInstances[equipmentInstanceId];
const design = state.enchantmentDesigns.find(d => d.id === designId);
const validation = CraftingApply.canApplyEnchantment(
instance,
design,
state.currentAction
);
if (!validation.canApply) return false;
set(() => ({
currentAction: 'enchant' as const,
applicationProgress: CraftingApply.initializeApplicationProgress(
equipmentInstanceId,
designId,
design!
),
}));
return true;
}
export function pauseApplication(
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => {
if (!state.applicationProgress) return {};
return {
applicationProgress: {
...state.applicationProgress,
paused: true,
},
};
});
}
export function resumeApplication(
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => {
if (!state.applicationProgress) return {};
return {
applicationProgress: {
...state.applicationProgress,
paused: false,
},
};
});
}
export function cancelApplication(
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set(() => ({
currentAction: 'meditate' as const,
applicationProgress: null,
}));
}
// ─── Disenchanting Actions ─────────────────────────────────────────────────
export function disenchantEquipment(
instanceId: string,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance || instance.enchantments.length === 0) return;
const disenchantLevel = 0;
const recoveryRate = 0.1 + disenchantLevel * 0.2;
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)],
}));
}
// ─── Equipment Crafting Actions ────────────────────────────────────────────
export function startCraftingEquipment(
blueprintId: string,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
): boolean {
const state = get();
const check = CraftingEquipment.canStartEquipmentCrafting(
blueprintId,
state.lootInventory.blueprints.includes(blueprintId),
state.lootInventory.materials,
state.rawMana,
state.currentAction
);
if (!check.canCraft) return false;
const result = CraftingEquipment.initializeEquipmentCrafting(
blueprintId,
state.lootInventory.materials,
state.rawMana
);
set((state) => ({
lootInventory: {
...state.lootInventory,
materials: result.newMaterials,
},
rawMana: state.rawMana - result.manaCost,
currentAction: 'craft' as const,
equipmentCraftingProgress: result.progress,
}));
return true;
}
export function cancelEquipmentCrafting(
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => {
const progress = state.equipmentCraftingProgress;
if (!progress) return { currentAction: 'meditate' as const, equipmentCraftingProgress: null };
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
progress.blueprintId,
progress.manaSpent
);
return {
currentAction: 'meditate' as const,
equipmentCraftingProgress: null,
rawMana: state.rawMana + cancelResult.manaRefund,
log: [cancelResult.logMessage, ...state.log.slice(0, 49)],
};
});
}
export function deleteMaterial(
materialId: string,
amount: number,
get: () => GameState,
set: (fn: (state: GameState) => Partial<GameState>) => void
) {
set((state) => {
const newMaterials = { ...state.lootInventory.materials };
const currentAmount = newMaterials[materialId] || 0;
const newAmount = Math.max(0, currentAmount - amount);
if (newAmount <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = newAmount;
}
return {
lootInventory: {
...state.lootInventory,
materials: newMaterials,
},
log: [`🗑️ Deleted ${amount}x ${materialId}.`, ...state.log.slice(0, 49)],
};
});
}
// ─── Computed Getters ──────────────────────────────────────────────────────
export function getEquipmentSpells(get: () => GameState): string[] {
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)];
}
export function getEquipmentEffects(get: () => GameState): Record<string, number> {
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;
}
export function getAvailableCapacity(
instanceId: string,
get: () => GameState
): number {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance) return 0;
return instance.totalCapacity - instance.usedCapacity;
}
+235
View File
@@ -0,0 +1,235 @@
// ─── Crafting Application System ────────────────────────────────────────────
// Application system functions extracted from crafting-slice.ts
import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types';
import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import { computeEffects } from './upgrade-effects';
import type { AttunementState } from './types';
import { calculateEnchantingXP } from './data/attunements';
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
// ─── Application Validation ─────────────────────────────────────────────────
// Check if enchantment application can start
export function canApplyEnchantment(
instance: EquipmentInstance | undefined,
design: EnchantmentDesign | undefined,
currentAction: string
): { canApply: boolean; reason?: string } {
if (!instance) {
return { canApply: false, reason: 'Equipment instance not found' };
}
if (!design) {
return { canApply: false, reason: 'Enchantment design not found' };
}
if (currentAction !== 'meditate') {
return { canApply: false, reason: 'Must be in meditate state' };
}
if (!instance.tags?.includes('Ready for Enchantment')) {
return { canApply: false, reason: 'Equipment must be prepared for enchanting' };
}
if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) {
return { canApply: false, reason: 'Not enough capacity on equipment' };
}
return { canApply: true };
}
// ─── Application Resource Calculation ────────────────────────────────────────
export interface ApplicationCosts {
time: number;
manaPerHour: number;
manaPerTick: number;
}
export function calculateApplicationCosts(design: EnchantmentDesign): ApplicationCosts {
const time = calculateApplicationTime(design);
const manaPerHour = calculateApplicationManaPerHour(design);
const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK
return { time, manaPerHour, manaPerTick };
}
// ─── Application Progress ───────────────────────────────────────────────────
// Initialize application progress
export function initializeApplicationProgress(
equipmentInstanceId: string,
designId: string,
design: EnchantmentDesign
): ApplicationProgress {
const costs = calculateApplicationCosts(design);
return {
equipmentInstanceId,
designId,
progress: 0,
required: costs.time,
manaPerHour: costs.manaPerHour,
paused: false,
manaSpent: 0,
};
}
// Calculate application progress after a tick
export interface ApplicationTickResult {
progress: number;
manaSpent: number;
manaConsumed: number;
isComplete: boolean;
triggeredFreeEnchant: boolean;
}
export function calculateApplicationTick(
currentProgress: number,
required: number,
currentManaSpent: number,
manaPerTick: number,
computedEffects: any
): ApplicationTickResult {
let progress = currentProgress + 0.04;
let manaSpent = currentManaSpent + manaPerTick;
let manaConsumed = manaPerTick;
let triggeredFreeEnchant = false;
let freeEnchantChance = 0;
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_PRESERVATION)) {
freeEnchantChance += 0.25;
}
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.THRIFTY_ENCHANTER)) {
freeEnchantChance += 0.10;
}
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING)) {
freeEnchantChance += 0.25;
}
if (freeEnchantChance > 0 && Math.random() < freeEnchantChance) {
progress = required;
manaConsumed = 0;
manaSpent = currentManaSpent;
triggeredFreeEnchant = true;
}
return {
progress,
manaSpent,
manaConsumed,
isComplete: progress >= required,
triggeredFreeEnchant,
};
}
// ─── Enchantment Application ────────────────────────────────────────────────
// Apply enchantments to equipment instance
export function applyEnchantments(
instance: EquipmentInstance,
design: EnchantmentDesign,
computedEffects: any
): {
updatedInstance: EquipmentInstance;
xpGained: number;
logMessage: string;
} {
const isPureEssenceActive = hasSpecial(computedEffects, SPECIAL_EFFECTS.PURE_ESSENCE);
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => {
let stacks = eff.stacks;
let actualCost = eff.capacityCost;
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (isPureEssenceActive && effectDef && effectDef.baseCapacityCost < 100) {
stacks = Math.ceil(stacks * 1.25);
}
return {
effectId: eff.effectId,
stacks,
actualCost,
};
});
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
const updatedInstance: EquipmentInstance = {
...instance,
enchantments: [...instance.enchantments, ...newEnchantments],
usedCapacity: instance.usedCapacity + design.totalCapacityUsed,
};
return {
updatedInstance,
xpGained,
logMessage: `✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`,
};
}
// ─── Attunement XP Updates ──────────────────────────────────────────────────
export function updateEnchanterAttunement(
attunements: Record<string, AttunementState>,
xpGained: number
): Record<string, AttunementState> {
if (!attunements?.enchanter?.active || xpGained <= 0) {
return attunements;
}
const enchanterState = attunements.enchanter;
let newXP = enchanterState.experience + xpGained;
let newLevel = enchanterState.level;
const { getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } = require('./data/attunements');
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
if (newXP >= xpNeeded) {
newXP -= xpNeeded;
newLevel++;
} else {
break;
}
}
return {
...attunements,
enchanter: {
...enchanterState,
level: newLevel,
experience: newXP,
},
};
}
// ─── Application Cancellation ────────────────────────────────────────────────
export function cancelApplication() {
return { logMessage: 'Enchantment application cancelled.' };
}
export function pauseApplication() {
return { logMessage: 'Enchantment application paused.' };
}
export function resumeApplication() {
return { logMessage: 'Enchantment application resumed.' };
}
// ─── Progress Calculations ──────────────────────────────────────────────────
export function getApplicationManaCostForTick(manaPerHour: number): number {
return manaPerHour * 0.04;
}
export function getApplicationRemainingTime(currentProgress: number, required: number): number {
return Math.max(0, required - currentProgress);
}
export function getApplicationCompletionPercent(currentProgress: number, required: number): number {
return Math.min(100, Math.floor((currentProgress / required) * 100));
}
+171
View File
@@ -0,0 +1,171 @@
// ─── Attunement System ──────────────────────────────────────────────────────
// Attunement system functions extracted from crafting-slice.ts
import type { AttunementState } from './types';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
// ─── Enchanter Attunement ──────────────────────────────────────────────────
export function initEnchanterAttunement(): AttunementState {
return {
id: 'enchanter',
active: true,
level: 1,
experience: 0,
title: 'Novice Enchanter',
};
}
export function isEnchanterActive(attunements: Record<string, AttunementState>): boolean {
return attunements.enchanter?.active === true;
}
// ─── XP Gain & Leveling ────────────────────────────────────────────────────
export function gainEnchantingXP(
attunements: Record<string, AttunementState>,
xpAmount: number
): {
attunements: Record<string, AttunementState>;
leveledUp: boolean;
previousLevel: number;
newLevel: number;
} {
if (!attunements?.enchanter?.active || xpAmount <= 0) {
return {
attunements,
leveledUp: false,
previousLevel: attunements.enchanter?.level || 0,
newLevel: attunements.enchanter?.level || 0,
};
}
const previousLevel = attunements.enchanter.level;
let newXP = attunements.enchanter.experience + xpAmount;
let newLevel = attunements.enchanter.level;
let leveledUp = false;
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
if (newXP >= xpNeeded) {
newXP -= xpNeeded;
newLevel++;
leveledUp = true;
} else {
break;
}
}
const updatedAttunements: Record<string, AttunementState> = {
...attunements,
enchanter: {
...attunements.enchanter,
level: newLevel,
experience: newXP,
},
};
return {
attunements: updatedAttunements,
leveledUp,
previousLevel,
newLevel,
};
}
export function getXpToNextLevel(currentLevel: number): number {
if (currentLevel >= MAX_ATTUNEMENT_LEVEL) return 0;
return getAttunementXPForLevel(currentLevel + 1);
}
export function getLevelProgress(attunements: Record<string, AttunementState>): {
currentLevel: number;
currentXP: number;
xpToNextLevel: number;
progressPercent: number;
maxLevel: number;
} {
const enchanter = attunements?.enchanter;
if (!enchanter?.active) {
return {
currentLevel: 0,
currentXP: 0,
xpToNextLevel: 0,
progressPercent: 0,
maxLevel: MAX_ATTUNEMENT_LEVEL,
};
}
const xpToNext = getXpToNextLevel(enchanter.level);
return {
currentLevel: enchanter.level,
currentXP: enchanter.experience,
xpToNextLevel: xpToNext,
progressPercent: xpToNext > 0 ? (enchanter.experience / xpToNext) * 100 : 100,
maxLevel: MAX_ATTUNEMENT_LEVEL,
};
}
export function calculateEnchantingXpFromDesignCapacity(capacityCost: number): number {
return calculateEnchantingXP(capacityCost);
}
export function calculateXpFromEquipmentInstance(
instance: { enchantments: Array<{ effectId: string; stacks: number; actualCost: number }> }
): number {
let totalXp = 0;
for (const ench of instance.enchantments) {
totalXp += calculateEnchantingXP(ench.actualCost);
}
return totalXp;
}
export function getEnchanterTitle(level: number): string {
if (level >= 10) return 'Grandmaster Enchanter';
if (level >= 8) return 'Master Enchanter';
if (level >= 6) return 'Expert Enchanter';
if (level >= 4) return 'Journeyman Enchanter';
if (level >= 2) return 'Apprentice Enchanter';
return 'Novice Enchanter';
}
export function updateEnchanterTitle(attunements: Record<string, AttunementState>): Record<string, AttunementState> {
if (!attunements.enchanter?.active) return attunements;
const title = getEnchanterTitle(attunements.enchanter.level);
return {
...attunements,
enchanter: { ...attunements.enchanter, title },
};
}
export function resetEnchanterExperience(attunements: Record<string, AttunementState>): Record<string, AttunementState> {
if (!attunements.enchanter) return attunements;
return {
...attunements,
enchanter: { ...attunements.enchanter, experience: 0, level: 1, title: 'Novice Enchanter' },
};
}
export function initAttunements(): Record<string, AttunementState> {
return { enchanter: initEnchanterAttunement() };
}
export function batchGainXP(
attunements: Record<string, AttunementState>,
xpAmounts: number[]
): { attunements: Record<string, AttunementState>; totalXP: number; totalLevelUps: number; finalLevel: number } {
let result = {
attunements,
totalXP: 0,
totalLevelUps: 0,
finalLevel: attunements.enchanter?.level || 1,
};
for (const xpAmount of xpAmounts) {
const gain = gainEnchantingXP(result.attunements, xpAmount);
result.attunements = gain.attunements;
result.totalXP += xpAmount;
if (gain.leveledUp) result.totalLevelUps++;
result.finalLevel = gain.newLevel;
}
return result;
}
+225
View File
@@ -0,0 +1,225 @@
// ─── Crafting Design System ─────────────────────────────────────────────────
// Design system functions: calculateDesignTime, capacity cost, XP, etc.
import type { EnchantmentDesign, DesignEffect, AppliedEnchantment } from './types';
import { calculateEnchantingXP } from './data/attunements';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import { computeEffects } from './upgrade-effects';
import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment';
// ─── Design Creation & Calculation ──────────────────────────────────────────
// Validate effects for a design against equipment category
export function validateDesignEffects(
effects: DesignEffect[],
equipmentTypeId: string,
enchantingLevel: number
): { valid: boolean; reason?: string } {
if (enchantingLevel < 1) {
return { valid: false, reason: 'Requires enchanting skill level 1' };
}
const equipType = EQUIPMENT_TYPES[equipmentTypeId];
if (!equipType) {
return { valid: false, reason: 'Invalid equipment type' };
}
const category = equipType.category;
if (!category) {
return { valid: false, reason: 'Invalid equipment category' };
}
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (!effectDef) {
return { valid: false, reason: `Unknown effect: ${eff.effectId}` };
}
if (!effectDef.allowedEquipmentCategories.includes(category)) {
return { valid: false, reason: `Effect ${eff.effectId} not allowed on ${category}` };
}
if (eff.stacks > effectDef.maxStacks) {
return { valid: false, reason: `Stacks exceed maximum for ${eff.effectId}` };
}
}
return { valid: true };
}
// Create an enchantment design from validated inputs
export function createEnchantmentDesign(
name: string,
equipmentType: string,
effects: DesignEffect[],
efficiencyBonus: number = 0
): EnchantmentDesign {
const totalCapacityUsed = calculateDesignCapacityCost(effects, efficiencyBonus);
const designTime = calculateDesignTime(effects);
return {
id: `design_${Date.now()}`,
name,
equipmentType,
effects,
totalCapacityUsed,
designTime,
created: Date.now(),
};
}
// ─── Capacity Cost Calculation ──────────────────────────────────────────────
export function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number {
return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0);
}
export function calculateTotalCapacityCost(design: EnchantmentDesign): number {
return design.totalCapacityUsed;
}
// ─── XP & Progression ───────────────────────────────────────────────────────
export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number {
return calculateEnchantingXP(design.totalCapacityUsed);
}
export function calculateXpFromInstanceEnchantments(
instance: { enchantments: AppliedEnchantment[] }
): number {
let totalXp = 0;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
const baseCost = effectDef?.baseCapacityCost || 0;
totalXp += calculateEnchantingXP(baseCost * ench.stacks);
}
return totalXp;
}
// ─── Design Time Calculations ──────────────────────────────────────────────
export function calculateDesignTime(effects: DesignEffect[]): number {
let time = 1;
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (effectDef) {
time += 0.5 * eff.stacks;
}
}
return time;
}
export function getDesignTimeWithHaste(
effects: DesignEffect[],
isRepeatDesign: boolean,
computedEffects: any
): number {
let time = calculateDesignTime(effects);
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
time *= 0.75;
}
return time;
}
// ─── Progress Calculations ──────────────────────────────────────────────────
export interface DesignProgressUpdate {
progress: number;
required: number;
isComplete: boolean;
timeBonus: number;
}
export function calculateDesignProgress(
currentProgress: number,
required: number,
computedEffects: any,
isRepeatDesign: boolean
): DesignProgressUpdate {
let progress = currentProgress + 0.04;
let timeBonus = 0;
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
timeBonus = 0.04 * 0.25;
progress += timeBonus;
}
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) {
progress = required;
}
return { progress, required, isComplete: progress >= required, timeBonus };
}
export function calculateSecondDesignProgress(
currentProgress: number,
required: number,
computedEffects: any,
isRepeatDesign: boolean
): DesignProgressUpdate {
return calculateDesignProgress(currentProgress, required, computedEffects, isRepeatDesign);
}
export function isSecondDesignSlotAvailable(
designProgress: any,
designProgress2: any,
hasEnchantMastery: boolean
): boolean {
if (!designProgress && !designProgress2) return true;
if (!designProgress && designProgress2) return false;
if (designProgress && !designProgress2 && hasEnchantMastery) return true;
return false;
}
// ─── Auto-save Completed Design ────────────────────────────────────────────
export function createCompletedDesignFromProgress(
progressData: {
designId: string;
name: string;
equipmentType: string;
effects: DesignEffect[];
required: number;
},
efficiencyBonus: number = 0
): EnchantmentDesign {
const totalCapacityCost = calculateDesignCapacityCost(progressData.effects, efficiencyBonus);
return {
id: progressData.designId,
name: progressData.name,
equipmentType: progressData.equipmentType,
effects: progressData.effects,
totalCapacityUsed: totalCapacityCost,
designTime: progressData.required,
created: Date.now(),
};
}
// ─── Design Management ──────────────────────────────────────────────────────
export interface DesignWithCapacityInfo {
design: EnchantmentDesign;
fitsInEquipment: boolean;
availableCapacity: number;
}
export function filterDesignsByEquipment(
designs: EnchantmentDesign[],
equipment: { instanceId: string; totalCapacity: number; usedCapacity: number } | null
): DesignWithCapacityInfo[] {
if (!equipment) return [];
return designs.map(design => ({
design,
fitsInEquipment: designFitsInEquipment(design, {
...equipment,
enchantments: [],
rarity: 'common',
quality: 100,
typeId: '',
name: '',
} as any),
availableCapacity: equipment.totalCapacity - equipment.usedCapacity,
}));
}
function designFitsInEquipment(design: EnchantmentDesign, instance: any): boolean {
return (instance.usedCapacity || 0) + design.totalCapacityUsed <= instance.totalCapacity;
}
+229
View File
@@ -0,0 +1,229 @@
// ─── Equipment Crafting System ──────────────────────────────────────────────
// Equipment crafting functions extracted from crafting-slice.ts
import type { EquipmentInstance, EquipmentCraftingProgress } from './types';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { EQUIPMENT_TYPES } from './data/equipment';
import { generateInstanceId } from './crafting-utils';
// ─── Equipment Crafting Validation ──────────────────────────────────────────
// Check if equipment crafting can start
export function canStartEquipmentCrafting(
blueprintId: string,
hasBlueprint: boolean,
materials: Record<string, number>,
currentMana: number,
currentAction: string
): { canCraft: boolean; reason?: string; recipe?: CraftingRecipe; missingMaterials?: Record<string, number>; missingMana?: number } {
if (currentAction !== 'meditate') {
return { canCraft: false, reason: 'Must be in meditate state' };
}
const recipe = CRAFTING_RECIPES[blueprintId];
if (!recipe) {
return { canCraft: false, reason: 'Invalid blueprint' };
}
if (!hasBlueprint) {
return { canCraft: false, reason: 'Blueprint not acquired' };
}
const { canCraft, missingMaterials } = canCraftRecipe(recipe, materials, currentMana);
if (!canCraft) {
const missingMana = Math.max(0, recipe.manaCost - currentMana);
return {
canCraft: false,
reason: missingMana > 0 ? 'Insufficient mana' : 'Missing materials',
recipe,
missingMaterials,
missingMana: missingMana > 0 ? missingMana : undefined,
};
}
return { canCraft: true, recipe };
}
// ─── Equipment Crafting Execution ───────────────────────────────────────────
// Deduct crafting costs and initialize progress
export interface CraftingInitResult {
recipe: CraftingRecipe;
newMaterials: Record<string, number>;
manaCost: number;
progress: EquipmentCraftingProgress;
}
export function initializeEquipmentCrafting(
blueprintId: string,
materials: Record<string, number>,
currentMana: number
): CraftingInitResult {
const recipe = CRAFTING_RECIPES[blueprintId];
// Deduct materials
const newMaterials = { ...materials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
if (newMaterials[matId] <= 0) {
delete newMaterials[matId];
}
}
// Create progress
const progress: EquipmentCraftingProgress = {
blueprintId,
equipmentTypeId: recipe.equipmentTypeId,
progress: 0,
required: recipe.craftTime,
manaSpent: recipe.manaCost,
};
return {
recipe,
newMaterials,
manaCost: recipe.manaCost,
progress,
};
}
// ─── Crafting Progress ──────────────────────────────────────────────────────
// Calculate crafting progress after a tick
export interface CraftingTickResult {
progress: number;
isComplete: boolean;
}
export function calculateCraftingTick(currentProgress: number, required: number): CraftingTickResult {
const progress = currentProgress + 0.04; // HOURS_PER_TICK
return {
progress,
isComplete: progress >= required,
};
}
// ─── Crafting Completion ───────────────────────────────────────────────────
// Create equipment instance from completed crafting
export function completeEquipmentCrafting(
blueprintId: string,
recipe: CraftingRecipe
): {
instanceId: string;
instance: EquipmentInstance;
logMessage: string;
} {
const equipType = EQUIPMENT_TYPES[recipe.equipmentTypeId];
if (!equipType) {
throw new Error(`Invalid equipment type: ${recipe.equipmentTypeId}`);
}
const instanceId = `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const newInstance: EquipmentInstance = {
instanceId,
typeId: recipe.equipmentTypeId,
name: recipe.name,
enchantments: [],
usedCapacity: 0,
totalCapacity: equipType.baseCapacity,
rarity: recipe.rarity,
quality: 100,
tags: [],
};
return {
instanceId,
instance: newInstance,
logMessage: `🔨 Crafted ${recipe.name}!`,
};
}
// ─── Crafting Cancellation ──────────────────────────────────────────────────
// Cancel active crafting and refund partial resources
export interface CraftingCancelResult {
manaRefund: number;
logMessage: string;
}
export function cancelEquipmentCrafting(blueprintId: string, manaSpent: number): CraftingCancelResult {
const recipe = CRAFTING_RECIPES[blueprintId];
if (!recipe) {
return {
manaRefund: 0,
logMessage: 'Invalid crafting recipe.',
};
}
// Refund 50% of mana
const manaRefund = Math.floor(manaSpent * 0.5);
return {
manaRefund,
logMessage: `🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.`,
};
}
// ─── Recipe Information ─────────────────────────────────────────────────────
export function getRecipe(blueprintId: string): CraftingRecipe | null {
return CRAFTING_RECIPES[blueprintId] || null;
}
export function getCraftableRecipes(
blueprints: string[],
materials: Record<string, number>,
currentMana: number
): CraftingRecipe[] {
const craftable: CraftingRecipe[] = [];
for (const blueprintId of blueprints) {
const recipe = CRAFTING_RECIPES[blueprintId];
if (!recipe) continue;
const { canCraft } = canCraftRecipe(recipe, materials, currentMana);
if (canCraft) {
craftable.push(recipe);
}
}
return craftable;
}
// ─── Material Management ────────────────────────────────────────────────────
// Delete materials from inventory
export function deleteMaterials(materialId: string, amount: number, materials: Record<string, number>): {
newMaterials: Record<string, number>;
deleted: number;
} {
const currentAmount = materials[materialId] || 0;
const deleted = Math.min(amount, currentAmount);
const remaining = Math.max(0, currentAmount - amount);
const newMaterials = { ...materials };
if (remaining <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = remaining;
}
return {
newMaterials,
deleted,
};
}
// Get total material count
export function getMaterialCount(materials: Record<string, number>, materialId: string): number {
return materials[materialId] || 0;
}
// Add materials to inventory
export function addMaterials(materials: Record<string, number>, materialId: string, amount: number): Record<string, number> {
const newMaterials = { ...materials };
newMaterials[materialId] = (newMaterials[materialId] || 0) + amount;
return newMaterials;
}
+277
View File
@@ -0,0 +1,277 @@
// ─── Crafting Loot Inventory System ────────────────────────────────────────────
// Loot inventory functions extracted from crafting-slice.ts
import type { LootInventory } from './types';
import type { CraftingRecipe } from './data/crafting-recipes';
// ─── Inventory Queries ──────────────────────────────────────────────────────
// Get material count
export function getMaterialCount(inventory: LootInventory, materialId: string): number {
return inventory.materials[materialId] || 0;
}
// Check if has blueprint
export function hasBlueprint(inventory: LootInventory, blueprintId: string): boolean {
return inventory.blueprints.includes(blueprintId);
}
// Check if has any materials
export function hasMaterials(inventory: LootInventory): boolean {
return Object.keys(inventory.materials).length > 0;
}
// Get total number of unique materials
export function getUniqueMaterialCount(inventory: LootInventory): number {
return Object.keys(inventory.materials).length;
}
// Get total material stacks (sum of all quantities)
export function getTotalMaterialStacks(inventory: LootInventory): number {
return Object.values(inventory.materials).reduce((sum, qty) => sum + qty, 0);
}
// ─── Inventory Modifications ────────────────────────────────────────────────
// Add materials to inventory
export function addMaterial(inventory: LootInventory, materialId: string, amount: number): LootInventory {
const currentAmount = inventory.materials[materialId] || 0;
return {
...inventory,
materials: {
...inventory.materials,
[materialId]: currentAmount + amount,
},
};
}
// Add multiple materials at once
export function addMaterials(
inventory: LootInventory,
materials: Record<string, number>
): LootInventory {
const newMaterials = { ...inventory.materials };
for (const [matId, amount] of Object.entries(materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) + amount;
}
return {
...inventory,
materials: newMaterials,
};
}
// Remove materials from inventory
export function removeMaterial(inventory: LootInventory, materialId: string, amount: number): {
inventory: LootInventory;
removed: number;
} {
const currentAmount = inventory.materials[materialId] || 0;
const removed = Math.min(amount, currentAmount);
const remaining = Math.max(0, currentAmount - amount);
const newMaterials = { ...inventory.materials };
if (remaining <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = remaining;
}
return {
inventory: {
...inventory,
materials: newMaterials,
},
removed,
};
}
// Delete materials completely (admin/debug operation)
export function deleteMaterial(inventory: LootInventory, materialId: string): LootInventory {
const newMaterials = { ...inventory.materials };
delete newMaterials[materialId];
return {
...inventory,
materials: newMaterials,
};
}
// Clear all materials
export function clearMaterials(inventory: LootInventory): LootInventory {
return {
...inventory,
materials: {},
};
}
// ─── Blueprint Management ──────────────────────────────────────────────────
// Add a blueprint
export function addBlueprint(inventory: LootInventory, blueprintId: string): LootInventory {
if (inventory.blueprints.includes(blueprintId)) {
return inventory;
}
return {
...inventory,
blueprints: [...inventory.blueprints, blueprintId],
};
}
// Remove a blueprint
export function removeBlueprint(inventory: LootInventory, blueprintId: string): LootInventory {
return {
...inventory,
blueprints: inventory.blueprints.filter(id => id !== blueprintId),
};
}
// Check and remove consumed blueprint (if single-use)
export function consumeBlueprint(inventory: LootInventory, blueprintId: string): LootInventory {
// Blueprints are persistent for now, but this function provides future flexibility
return inventory;
}
// ─── Recipe Material Checks ─────────────────────────────────────────────────
// Check if specific materials are available
export function checkMaterialsAvailable(
inventory: LootInventory,
required: Record<string, number>
): { hasAll: boolean; missing: Record<string, number> } {
const missing: Record<string, number> = {};
let hasAll = true;
for (const [matId, amount] of Object.entries(required)) {
const available = inventory.materials[matId] || 0;
if (available < amount) {
missing[matId] = amount - available;
hasAll = false;
}
}
return { hasAll, missing };
}
// Deduct recipe materials
export function deductRecipeMaterials(
inventory: LootInventory,
recipeMaterials: Record<string, number>
): LootInventory {
const newMaterials = { ...inventory.materials };
for (const [matId, amount] of Object.entries(recipeMaterials)) {
const newAmount = (newMaterials[matId] || 0) - amount;
if (newAmount <= 0) {
delete newMaterials[matId];
} else {
newMaterials[matId] = newAmount;
}
}
return {
...inventory,
materials: newMaterials,
};
}
// ─── Inventory Filters & Sorting ────────────────────────────────────────────
export interface MaterialWithInfo {
id: string;
quantity: number;
name: string;
rarity: string;
}
// Get all materials with their info
export function getAllMaterials(inventory: LootInventory): MaterialWithInfo[] {
return Object.entries(inventory.materials).map(([id, quantity]) => ({
id,
quantity,
name: id, // Could be replaced with lookup from LOOT_DROPS
rarity: 'common', // Could be replaced with lookup
}));
}
// Filter materials by minimum quantity
export function filterMaterialsByQuantity(
inventory: LootInventory,
minQuantity: number
): Record<string, number> {
const filtered: Record<string, number> = {};
for (const [id, qty] of Object.entries(inventory.materials)) {
if (qty >= minQuantity) {
filtered[id] = qty;
}
}
return filtered;
}
// ─── Crafting Availability ─────────────────────────────────────────────────
// Check if can craft a recipe
export function canCraftFromInventory(
inventory: LootInventory,
recipe: CraftingRecipe,
currentMana: number
): { canCraft: boolean; reason?: string; missingMaterials?: Record<string, number>; missingMana?: number } {
const { hasAll, missing } = checkMaterialsAvailable(inventory, recipe.materials);
if (!hasAll) {
return { canCraft: false, missingMaterials: missing };
}
if (currentMana < recipe.manaCost) {
return {
canCraft: false,
missingMana: recipe.manaCost - currentMana,
reason: 'Insufficient mana',
};
}
if (!inventory.blueprints.includes(recipe.id)) {
return { canCraft: false, reason: 'Blueprint not acquired' };
}
return { canCraft: true };
}
// Get all craftable recipes from inventory
export function getCraftableRecipes(
inventory: LootInventory,
recipes: Record<string, CraftingRecipe>,
currentMana: number
): CraftingRecipe[] {
const craftable: CraftingRecipe[] = [];
for (const blueprintId of inventory.blueprints) {
const recipe = recipes[blueprintId];
if (!recipe) continue;
const { canCraft } = canCraftFromInventory(inventory, recipe, currentMana);
if (canCraft) {
craftable.push(recipe);
}
}
return craftable;
}
// ─── Inventory Statistics ──────────────────────────────────────────────────
export interface InventoryStats {
totalUniqueMaterials: number;
totalMaterialStacks: number;
totalBlueprints: number;
craftableItems: string[];
}
export function getInventoryStats(inventory: LootInventory): InventoryStats {
const totalUniqueMaterials = Object.keys(inventory.materials).length;
const totalMaterialStacks = Object.values(inventory.materials).reduce((sum, qty) => sum + qty, 0);
const totalBlueprints = inventory.blueprints.length;
return {
totalUniqueMaterials,
totalMaterialStacks,
totalBlueprints,
craftableItems: [], // Would need recipes to compute
};
}
+140
View File
@@ -0,0 +1,140 @@
// ─── Crafting Preparation System ────────────────────────────────────────────
// Preparation system functions extracted from crafting-slice.ts
import type { EquipmentInstance, PreparationProgress } from './types';
import { calculatePrepTime, calculatePrepManaCost, calculateManaPerHourForPrep } from './crafting-utils';
// ─── Preparation Validation ─────────────────────────────────────────────────
// Check if an equipment instance can be prepared
export function canPrepareEquipment(
instance: EquipmentInstance | undefined,
currentTags: string[]
): { canPrepare: boolean; reason?: string } {
if (!instance) {
return { canPrepare: false, reason: 'Equipment instance not found' };
}
if (currentTags.includes('Ready for Enchantment')) {
return { canPrepare: false, reason: 'Equipment is already prepared for enchanting' };
}
return { canPrepare: true };
}
// Calculate preparation resource costs
export interface PreparationCosts {
time: number;
manaTotal: number;
manaPerHour: number;
manaPerTick: number;
}
export function calculatePreparationCosts(totalCapacity: number): PreparationCosts {
const time = calculatePrepTime(totalCapacity);
const manaTotal = calculatePrepManaCost(totalCapacity);
const manaPerHour = calculateManaPerHourForPrep(totalCapacity, time);
const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK
return { time, manaTotal, manaPerHour, manaPerTick };
}
// ─── Preparation Progress ───────────────────────────────────────────────────
// Initialize preparation progress
export function initializePreparationProgress(
equipmentInstanceId: string,
totalCapacity: number,
manaCostPaid: number = 0
): PreparationProgress {
const costs = calculatePreparationCosts(totalCapacity);
return {
equipmentInstanceId,
progress: 0,
required: costs.time,
manaCostPaid,
};
}
// Calculate updated preparation progress after a tick
export interface PreparationTickResult {
progress: number;
manaCostPaid: number;
manaConsumed: number;
isComplete: boolean;
}
export function calculatePreparationTick(
currentProgress: number,
required: number,
manaPerTick: number
): PreparationTickResult {
const progress = currentProgress + 0.04; // HOURS_PER_TICK
const manaConsumed = manaPerTick;
const manaCostPaid = manaPerTick; // Accumulated
return {
progress,
manaCostPaid,
manaConsumed,
isComplete: progress >= required,
};
}
// ─── Preparation Completion ─────────────────────────────────────────────────
// Apply preparation completion to equipment instance
export function completePreparation(
instance: EquipmentInstance,
manaSpent: number
): {
updatedInstance: EquipmentInstance;
manaRecovered: number;
logMessage: string;
} {
// Calculate mana recovery from disenchanting (disenchanting skill removed - Bug 13)
const disenchantLevel = 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);
}
const updatedInstance: EquipmentInstance = {
...instance,
enchantments: [],
usedCapacity: 0,
rarity: 'common',
tags: [...(instance.tags || []), 'Ready for Enchantment'],
};
return {
updatedInstance,
manaRecovered: totalRecovered,
logMessage: `✅ Equipment prepared for enchanting! Recovered ${totalRecovered} mana.`,
};
}
// Cancel preparation (no resource recovery for preparation itself)
export function cancelPreparation() {
return {
logMessage: 'Preparation cancelled.',
};
}
// ─── Preparation State Calculations ─────────────────────────────────────────
export function getPreparationManaCostForTick(instance: EquipmentInstance): number {
const costs = calculatePreparationCosts(instance.totalCapacity);
return costs.manaPerTick;
}
export function getPreparationRemainingTime(currentProgress: number, required: number): number {
return Math.max(0, required - currentProgress);
}
export function getPreparationCompletionPercent(currentProgress: number, required: number): number {
return Math.min(100, Math.floor((currentProgress / required) * 100));
}
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
// ─── Crafting Helper Utilities ─────────────────────────────────────────────────
// Instance/ID generation and helper functions extracted from crafting-slice.ts
import type { EquipmentInstance, EnchantmentDesign, DesignEffect } from './types';
import { EQUIPMENT_TYPES, type EquipmentCategory, type EquipmentSlot } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
// ─── Instance/ID Generation ──────────────────────────────────────────────────
export function generateInstanceId(): string {
return `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export function generateDesignId(): string {
return `design_${Date.now()}`;
}
// ─── Equipment Category & Type Helpers ───────────────────────────────────────
export function getEquipmentCategory(typeId: string): EquipmentCategory | null {
const type = EQUIPMENT_TYPES[typeId];
return type?.category || null;
}
export function getEquipmentType(typeId: string) {
return EQUIPMENT_TYPES[typeId] || null;
}
// ─── Capacity Calculation Helpers ────────────────────────────────────────────
export function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number {
return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0);
}
export function getAvailableCapacity(instance: EquipmentInstance): number {
return instance.totalCapacity - instance.usedCapacity;
}
export function designFitsInEquipment(design: EnchantmentDesign, instance: EquipmentInstance): boolean {
return instance.usedCapacity + design.totalCapacityUsed <= instance.totalCapacity;
}
// ─── Time Calculation Helpers ────────────────────────────────────────────────
export function calculateDesignTime(effects: DesignEffect[]): number {
let time = 1;
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (effectDef) {
time += 0.5 * eff.stacks;
}
}
return time;
}
export function calculatePrepTime(equipmentCapacity: number): number {
return 2 + Math.floor(equipmentCapacity / 50);
}
export function calculateApplicationTime(design: EnchantmentDesign): number {
return 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0);
}
// ─── Mana Cost Calculation Helpers ───────────────────────────────────────────
export function calculatePrepManaCost(equipmentCapacity: number): number {
return equipmentCapacity * 10;
}
export function calculateApplicationManaPerHour(design: EnchantmentDesign): number {
return 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0);
}
export function calculateManaPerHourForPrep(equipmentCapacity: number, prepTime: number): number {
return calculatePrepManaCost(equipmentCapacity) / prepTime;
}
// ─── Recipe & Crafting Helpers ───────────────────────────────────────────────
export function checkRecipeMaterials(
recipe: CraftingRecipe,
materials: Record<string, number>
): { canCraft: boolean; missingMaterials: Record<string, number> } {
const missingMaterials: Record<string, number> = {};
let canCraft = true;
for (const [matId, amount] of Object.entries(recipe.materials)) {
const have = materials[matId] || 0;
if (have < amount) {
missingMaterials[matId] = amount - have;
canCraft = false;
}
}
return { canCraft, missingMaterials };
}
export function deductRecipeMaterials(
recipe: CraftingRecipe,
materials: Record<string, number>
): Record<string, number> {
const newMaterials = { ...materials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
if (newMaterials[matId] <= 0) {
delete newMaterials[matId];
}
}
return newMaterials;
}
export function refundCraftMaterials(recipe: CraftingRecipe, refundRate: number = 0.5): Record<string, number> {
const refunds: Record<string, number> = {};
for (const [matId, amount] of Object.entries(recipe.materials)) {
refunds[matId] = Math.floor(amount * refundRate);
}
return refunds;
}
// ─── Validation Helpers ──────────────────────────────────────────────────────
export function canEquipInSlot(
instance: EquipmentInstance,
slot: EquipmentSlot,
currentlyEquipped: Record<EquipmentSlot, string | null>
): boolean {
const type = EQUIPMENT_TYPES[instance.typeId];
if (!type) return false;
const validSlots = type.category === 'accessory'
? ['accessory1', 'accessory2']
: [type.slot];
if (!validSlots.includes(slot)) return false;
if (currentlyEquipped[slot] === instance.instanceId) return true;
const isTwoHanded = type.twoHanded === true;
if (isTwoHanded) {
if (currentlyEquipped.mainHand || currentlyEquipped.offHand) {
return false;
}
if (slot !== 'mainHand') return false;
}
if (slot === 'offHand' && currentlyEquipped.mainHand) {
const mainHandType = EQUIPMENT_TYPES[currentlyEquipped.mainHand];
if (mainHandType?.twoHanded) {
return false;
}
}
return true;
}
export function isTwoHanded(typeId: string): boolean {
return EQUIPMENT_TYPES[typeId]?.twoHanded === true;
}
+12
View File
@@ -6,6 +6,18 @@ export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'sword' | 'he
// All equipment slots in order // All equipment slots in order
export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2']; export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2'];
// Human-readable names for equipment slots
export const SLOT_NAMES: Record<EquipmentSlot, string> = {
mainHand: 'Main Hand',
offHand: 'Off Hand',
head: 'Head',
body: 'Body',
hands: 'Hands',
feet: 'Feet',
accessory1: 'Accessory 1',
accessory2: 'Accessory 2',
};
export interface EquipmentType { export interface EquipmentType {
id: string; id: string;
name: string; name: string;
+66 -60
View File
@@ -1,64 +1,70 @@
import type { GameStore } from '@/lib/game/store'; // ─── Game Types (Barrel Re-Exports) ──────────────────────────────────────────
import type { SPELLS } from '@/lib/game/constants'; // Re-exports all core game types from the types/ subdirectory
import type { EquipmentSpellState } from '@/lib/game/state';
import type { RoomType, ActivityLogEntry } from '@/lib/game/types/game';
// Re-export ActivityLogEntry for convenience // ─── Core Game Types (re-exported from types/ subdirectory) ─────────────────
export { ActivityLogEntry };
// Room Display Props export type {
export interface RoomDisplayProps { ElementCategory,
roomType: RoomType; ElementDef,
roomConfig: { label: string; icon: string; color: string }; ElementState,
primaryEnemy: any; } from './types/elements';
swarmEnemies: any[];
puzzleId?: string;
puzzleProgress?: number;
simpleMode: boolean;
floorElemDef: any;
floorHP: number;
floorMaxHP: number;
totalDPS: number;
currentAction: string | null;
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
}
// Floor Controls Props export type {
export interface FloorControlsProps { AttunementSlot,
store: GameStore; AttunementDef,
climbDirection: 'up' | 'down' | null; AttunementState,
isGuardianFloor: boolean; GuardianBoon,
currentRoom: any; GuardianDef,
currentGuardian: any; } from './types/attunements';
isFloorCleared: boolean;
floorElemDef: any;
roomType: RoomType;
roomConfig: { label: string; icon: string; color: string };
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
upgradeEffects: any;
floorElem: string;
totalDPS: number;
getEnemyName: typeof import('@/lib/game/store').getEnemyName;
calcDamage: typeof import('@/lib/game/store').calcDamage;
SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF;
canCastSpell: (spellId: string) => boolean;
storeCurrentAction: string | null;
handleClimb: (direction: 'up' | 'down') => void;
formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost;
getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor;
}
// Combat Stats Panel Props export type {
export interface CombatStatsPanelProps { SpellCost,
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>; SpellDef,
store: GameStore; SpellEffect,
totalDPS: number; SpellState,
calcDamage: typeof import('@/lib/game/store').calcDamage; } from './types/spells';
formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost;
getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor; export type {
SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF; SkillDef,
upgradeEffects: any; SkillUpgradeDef,
canCastSpell: (spellId: string) => boolean; SkillUpgradeEffect,
studySpeedMult: number; SkillEvolutionPath,
storeCurrentAction: string | null; SkillTierDef,
} SkillPerkChoice,
SkillUpgradeChoice,
PrestigeDef,
SkillCost,
} from './types/skills';
export type {
EquipmentDef,
EquipmentInstance,
AppliedEnchantment,
EnchantmentDesign,
DesignEffect,
DesignProgress,
PreparationProgress,
ApplicationProgress,
EquipmentCraftingProgress,
EquipmentSpellState,
BlueprintDef,
LootInventory,
} from './types/equipment';
export type {
RoomType,
EnemyState,
FloorState,
AchievementDef,
AchievementState,
GameAction,
ScheduleBlock,
StudyTarget,
SummonedGolem,
GolemancyState,
GameState,
GameActionType,
ActivityEventType,
ActivityLogEntry,
EquipmentSpellState,
} from './types/game';
+1
View File
@@ -26,6 +26,7 @@ export interface AttunementState {
active: boolean; // Whether this attunement is currently active active: boolean; // Whether this attunement is currently active
level: number; // Attunement level (for future progression) level: number; // Attunement level (for future progression)
experience: number; // Progress toward next level experience: number; // Progress toward next level
title?: string; // Title based on level (e.g., 'Novice Enchanter')
} }
// Boon types that guardians can grant // Boon types that guardians can grant