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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m51s
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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)]',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
+177
-863
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user