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';
|
||||
|
||||
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 { GameCard } from '@/components/ui/game-card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useMemo } from 'react';
|
||||
import {
|
||||
EQUIPMENT_TYPES,
|
||||
EQUIPMENT_SLOTS,
|
||||
SLOT_NAMES,
|
||||
getEquipmentBySlot,
|
||||
type EquipmentSlot,
|
||||
type EquipmentType,
|
||||
@@ -23,18 +24,6 @@ import { EnchantmentsPanel } from './EnchantmentsPanel';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
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
|
||||
export const RARITY_BORDER_COLORS: Record<string, string> = {
|
||||
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));
|
||||
}
|
||||
+183
-869
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
|
||||
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 {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
+66
-60
@@ -1,64 +1,70 @@
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import type { SPELLS } from '@/lib/game/constants';
|
||||
import type { EquipmentSpellState } from '@/lib/game/state';
|
||||
import type { RoomType, ActivityLogEntry } from '@/lib/game/types/game';
|
||||
// ─── Game Types (Barrel Re-Exports) ──────────────────────────────────────────
|
||||
// Re-exports all core game types from the types/ subdirectory
|
||||
|
||||
// Re-export ActivityLogEntry for convenience
|
||||
export { ActivityLogEntry };
|
||||
// ─── Core Game Types (re-exported from types/ subdirectory) ─────────────────
|
||||
|
||||
// Room Display Props
|
||||
export interface RoomDisplayProps {
|
||||
roomType: RoomType;
|
||||
roomConfig: { label: string; icon: string; color: string };
|
||||
primaryEnemy: any;
|
||||
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 }>;
|
||||
}
|
||||
export type {
|
||||
ElementCategory,
|
||||
ElementDef,
|
||||
ElementState,
|
||||
} from './types/elements';
|
||||
|
||||
// Floor Controls Props
|
||||
export interface FloorControlsProps {
|
||||
store: GameStore;
|
||||
climbDirection: 'up' | 'down' | null;
|
||||
isGuardianFloor: boolean;
|
||||
currentRoom: any;
|
||||
currentGuardian: any;
|
||||
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;
|
||||
}
|
||||
export type {
|
||||
AttunementSlot,
|
||||
AttunementDef,
|
||||
AttunementState,
|
||||
GuardianBoon,
|
||||
GuardianDef,
|
||||
} from './types/attunements';
|
||||
|
||||
// Combat Stats Panel Props
|
||||
export interface CombatStatsPanelProps {
|
||||
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
|
||||
store: GameStore;
|
||||
totalDPS: number;
|
||||
calcDamage: typeof import('@/lib/game/store').calcDamage;
|
||||
formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost;
|
||||
getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor;
|
||||
SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF;
|
||||
upgradeEffects: any;
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
studySpeedMult: number;
|
||||
storeCurrentAction: string | null;
|
||||
}
|
||||
export type {
|
||||
SpellCost,
|
||||
SpellDef,
|
||||
SpellEffect,
|
||||
SpellState,
|
||||
} from './types/spells';
|
||||
|
||||
export type {
|
||||
SkillDef,
|
||||
SkillUpgradeDef,
|
||||
SkillUpgradeEffect,
|
||||
SkillEvolutionPath,
|
||||
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
|
||||
level: number; // Attunement level (for future progression)
|
||||
experience: number; // Progress toward next level
|
||||
title?: string; // Title based on level (e.g., 'Novice Enchanter')
|
||||
}
|
||||
|
||||
// Boon types that guardians can grant
|
||||
|
||||
Reference in New Issue
Block a user