Fix crafting tab crash, update attunement XP system
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 33s

- Add missing EquipmentCraftingProgress interface to types.ts
- Fix CraftingTab to accept store prop like other tabs
- Update attunement XP calculation: 1 XP per 10 capacity used (min 1)
- Change XP requirements: 1000 for lv2, 2500 for lv3, doubling thereafter
- Grant Enchanter XP when enchantments are applied
- Add equipmentCraftingProgress to GameState and persist config
This commit is contained in:
2026-03-27 19:23:00 +00:00
parent 49fe47948f
commit 98309fbc85
5 changed files with 93 additions and 74 deletions

View File

@@ -17,7 +17,7 @@ import { ENCHANTMENT_EFFECTS, type EnchantmentEffectDef, calculateEffectCapacity
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import { fmt } from '@/lib/game/store'; import { fmt, type GameStore } from '@/lib/game/store';
// Slot display names // Slot display names
const SLOT_NAMES: Record<EquipmentSlot, string> = { const SLOT_NAMES: Record<EquipmentSlot, string> = {
@@ -31,76 +31,38 @@ const SLOT_NAMES: Record<EquipmentSlot, string> = {
accessory2: 'Accessory 2', accessory2: 'Accessory 2',
}; };
interface CraftingTabProps { export interface CraftingTabProps {
// Equipment state store: GameStore;
equippedInstances: Record<string, string | null>;
equipmentInstances: Record<string, EquipmentInstance>;
enchantmentDesigns: EnchantmentDesign[];
// Progress states
designProgress: { designId: string; progress: number; required: number } | null;
preparationProgress: { equipmentInstanceId: string; progress: number; required: number; manaCostPaid: number } | null;
applicationProgress: { equipmentInstanceId: string; designId: string; progress: number; required: number; manaPerHour: number; paused: boolean; manaSpent: number } | null;
equipmentCraftingProgress: EquipmentCraftingProgress | null;
// Player state
rawMana: number;
skills: Record<string, number>;
currentAction: string;
unlockedEffects: string[]; // Effect IDs that have been researched
// Loot inventory
lootInventory: LootInventory;
// Actions
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
cancelDesign: () => void;
saveDesign: (design: EnchantmentDesign) => void;
deleteDesign: (designId: string) => void;
startPreparing: (equipmentInstanceId: string) => boolean;
cancelPreparation: () => void;
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
pauseApplication: () => void;
resumeApplication: () => void;
cancelApplication: () => void;
disenchantEquipment: (instanceId: string) => void;
getAvailableCapacity: (instanceId: string) => number;
// Equipment crafting actions
startCraftingEquipment: (blueprintId: string) => boolean;
cancelEquipmentCrafting: () => void;
deleteMaterial: (materialId: string, amount: number) => void;
} }
export function CraftingTab({ export function CraftingTab({ store }: CraftingTabProps) {
equippedInstances, const equippedInstances = store.equippedInstances;
equipmentInstances, const equipmentInstances = store.equipmentInstances;
enchantmentDesigns, const enchantmentDesigns = store.enchantmentDesigns;
designProgress, const designProgress = store.designProgress;
preparationProgress, const preparationProgress = store.preparationProgress;
applicationProgress, const applicationProgress = store.applicationProgress;
equipmentCraftingProgress, const equipmentCraftingProgress = store.equipmentCraftingProgress;
rawMana, const rawMana = store.rawMana;
skills, const skills = store.skills;
currentAction, const currentAction = store.currentAction;
unlockedEffects, const unlockedEffects = store.unlockedEffects;
lootInventory, const lootInventory = store.lootInventory;
startDesigningEnchantment, const startDesigningEnchantment = store.startDesigningEnchantment;
cancelDesign, const cancelDesign = store.cancelDesign;
saveDesign, const saveDesign = store.saveDesign;
deleteDesign, const deleteDesign = store.deleteDesign;
startPreparing, const startPreparing = store.startPreparing;
cancelPreparation, const cancelPreparation = store.cancelPreparation;
startApplying, const startApplying = store.startApplying;
pauseApplication, const pauseApplication = store.pauseApplication;
resumeApplication, const resumeApplication = store.resumeApplication;
cancelApplication, const cancelApplication = store.cancelApplication;
disenchantEquipment, const disenchantEquipment = store.disenchantEquipment;
getAvailableCapacity, const getAvailableCapacity = store.getAvailableCapacity;
startCraftingEquipment, const startCraftingEquipment = store.startCraftingEquipment;
cancelEquipmentCrafting, const cancelEquipmentCrafting = store.cancelEquipmentCrafting;
deleteMaterial, const deleteMaterial = store.deleteMaterial;
}: CraftingTabProps) {
const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft'); const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft');
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null); const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null); const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);

View File

@@ -1,11 +1,12 @@
// ─── Crafting Store Slice ───────────────────────────────────────────────────────── // ─── Crafting Store Slice ─────────────────────────────────────────────────────────
// Handles equipment and enchantment system: design, prepare, apply stages // Handles equipment and enchantment system: design, prepare, apply stages
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory } from './types'; import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment'; import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes'; import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants'; import { SPELLS_DEF } from './constants';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
// ─── Helper Functions ───────────────────────────────────────────────────────── // ─── Helper Functions ─────────────────────────────────────────────────────────
@@ -687,11 +688,42 @@ export function processCraftingTick(
actualCost: eff.capacityCost, actualCost: eff.capacityCost,
})); }));
// Calculate and grant attunement XP to enchanter
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
let newAttunements = state.attunements;
if (state.attunements.enchanter?.active && xpGained > 0) {
const enchanterState = state.attunements.enchanter;
let newXP = enchanterState.experience + xpGained;
let newLevel = enchanterState.level;
// Check for level ups
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
if (newXP >= xpNeeded) {
newXP -= xpNeeded;
newLevel++;
} else {
break;
}
}
newAttunements = {
...state.attunements,
enchanter: {
...enchanterState,
level: newLevel,
experience: newXP,
},
};
}
updates = { updates = {
...updates, ...updates,
rawMana: rawMana - manaCost, rawMana: rawMana - manaCost,
applicationProgress: null, applicationProgress: null,
currentAction: 'meditate', currentAction: 'meditate',
attunements: newAttunements,
equipmentInstances: { equipmentInstances: {
...state.equipmentInstances, ...state.equipmentInstances,
[app.equipmentInstanceId]: { [app.equipmentInstanceId]: {
@@ -700,7 +732,7 @@ export function processCraftingTick(
usedCapacity: instance.usedCapacity + design.totalCapacityUsed, usedCapacity: instance.usedCapacity + design.totalCapacityUsed,
}, },
}, },
log: [`✨ Enchantment "${design.name}" applied to ${instance.name}!`, ...log], log: [`✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, ...log],
}; };
} }
} else { } else {

View File

@@ -111,10 +111,22 @@ export function getAttunementConversionRate(attunementId: string, level: number)
// XP required for attunement level // XP required for attunement level
export function getAttunementXPForLevel(level: number): number { export function getAttunementXPForLevel(level: number): number {
// Level 2: 100 XP, Level 3: 300 XP, Level 4: 900 XP, etc. // New scaling:
// Exponential: 100 * (3 ^ (level - 2)) // Level 2: 1000 XP
// Level 3: 2500 XP
// Level 4: 5000 XP
// Level 5: 10000 XP
// etc. (each level requires 2x the previous, starting from 1000)
if (level <= 1) return 0; if (level <= 1) return 0;
return Math.floor(100 * Math.pow(3, level - 2)); if (level === 2) return 1000;
// For level 3+: 1000 * 2.5^(level-2), but rounded nicely
return Math.floor(1000 * Math.pow(2, level - 2) * (level >= 3 ? 1.25 : 1));
}
// Calculate XP gained from enchanting based on capacity used
export function calculateEnchantingXP(capacityUsed: number): number {
// 1 XP per 10 capacity used, floored, minimum 1
return Math.max(1, Math.floor(capacityUsed / 10));
} }
// Max attunement level // Max attunement level

View File

@@ -499,6 +499,7 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
designProgress: null, designProgress: null,
preparationProgress: null, preparationProgress: null,
applicationProgress: null, applicationProgress: null,
equipmentCraftingProgress: null,
unlockedEffects: [...BASE_UNLOCKED_EFFECTS], // Start with mana bolt only unlockedEffects: [...BASE_UNLOCKED_EFFECTS], // Start with mana bolt only
equipmentSpellStates: [], equipmentSpellStates: [],
@@ -1875,6 +1876,8 @@ export const useGameStore = create<GameStore>()(
designProgress: state.designProgress, designProgress: state.designProgress,
preparationProgress: state.preparationProgress, preparationProgress: state.preparationProgress,
applicationProgress: state.applicationProgress, applicationProgress: state.applicationProgress,
equipmentCraftingProgress: state.equipmentCraftingProgress,
unlockedEffects: state.unlockedEffects,
// Loot inventory // Loot inventory
lootInventory: state.lootInventory, lootInventory: state.lootInventory,
}), }),

View File

@@ -232,6 +232,15 @@ export interface ApplicationProgress {
manaSpent: number; // Total mana spent so far manaSpent: number; // Total mana spent so far
} }
// Equipment crafting progress (from blueprints)
export interface EquipmentCraftingProgress {
blueprintId: string;
equipmentTypeId: string;
progress: number; // Hours spent crafting
required: number; // Total hours needed
manaSpent: number; // Total mana spent so far
}
// Equipment spell state (for multi-spell casting) // Equipment spell state (for multi-spell casting)
export interface EquipmentSpellState { export interface EquipmentSpellState {
spellId: string; spellId: string;
@@ -362,6 +371,7 @@ export interface GameState {
designProgress: DesignProgress | null; designProgress: DesignProgress | null;
preparationProgress: PreparationProgress | null; preparationProgress: PreparationProgress | null;
applicationProgress: ApplicationProgress | null; applicationProgress: ApplicationProgress | null;
equipmentCraftingProgress: EquipmentCraftingProgress | null;
// Unlocked enchantment effects for designing // Unlocked enchantment effects for designing
unlockedEffects: string[]; // Effect IDs that have been researched unlockedEffects: string[]; // Effect IDs that have been researched