- {/* Materials */}
- {Object.entries(inventory.materials).length > 0 && (
-
-
-
- Materials
-
-
- {Object.entries(inventory.materials).map(([id, count]) => {
- const drop = LOOT_DROPS[id];
- if (!drop || count <= 0) return null;
- const rarityStyle = RARITY_COLORS[drop.rarity];
- return (
-
-
- {drop.name}
-
-
- x{count}
-
-
- );
- })}
-
-
- )}
-
- {/* Blueprints */}
- {inventory.blueprints.length > 0 && (
-
-
-
- Blueprints Discovered
-
-
- {inventory.blueprints.map((id) => {
- const drop = LOOT_DROPS[id];
- if (!drop) return null;
- const rarityStyle = RARITY_COLORS[drop.rarity];
- return (
-
- {drop.name}
-
- );
- })}
-
-
- )}
+ <>
+
+
+
+
+ Inventory
+
+ {totalItems} items
+
+
+
+
+ {/* Search and Filter Controls */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="h-7 pl-7 bg-gray-800/50 border-gray-700 text-xs"
+ />
+
+
-
-
-
+
+ {/* Filter Tabs */}
+
+ {[
+ { mode: 'all' as FilterMode, label: 'All' },
+ { mode: 'materials' as FilterMode, label: `Materials (${materialCount})` },
+ { mode: 'essence' as FilterMode, label: `Essence (${essenceCount})` },
+ { mode: 'blueprints' as FilterMode, label: `Blueprints (${blueprintCount})` },
+ { mode: 'equipment' as FilterMode, label: `Equipment (${equipmentCount})` },
+ ].map(({ mode, label }) => (
+
+ ))}
+
+
+
+
+
+
+ {/* Materials */}
+ {(filterMode === 'all' || filterMode === 'materials') && filteredMaterials.length > 0 && (
+
+
+
+ Materials
+
+
+ {filteredMaterials.map(([id, count]) => {
+ const drop = LOOT_DROPS[id];
+ if (!drop) return null;
+ const rarityStyle = RARITY_COLORS[drop.rarity];
+ return (
+
+
+
+
+ {drop.name}
+
+
+ x{count}
+
+
+ {drop.rarity}
+
+
+ {onDeleteMaterial && (
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Essence */}
+ {(filterMode === 'all' || filterMode === 'essence') && filteredEssence.length > 0 && (
+
+
+
+ Elemental Essence
+
+
+ {filteredEssence.map(([id, state]) => {
+ const elem = ELEMENTS[id];
+ if (!elem) return null;
+ return (
+
+
+ {elem.sym}
+
+ {elem.name}
+
+
+
+ {state.current} / {state.max}
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Blueprints */}
+ {(filterMode === 'all' || filterMode === 'blueprints') && inventory.blueprints.length > 0 && (
+
+
+
+ Blueprints (permanent)
+
+
+ {inventory.blueprints.map((id) => {
+ const drop = LOOT_DROPS[id];
+ if (!drop) return null;
+ const rarityStyle = RARITY_COLORS[drop.rarity];
+ return (
+
+ {drop.name}
+
+ );
+ })}
+
+
+ Blueprints are permanent unlocks - use them to craft equipment
+
+
+ )}
+
+ {/* Equipment */}
+ {(filterMode === 'all' || filterMode === 'equipment') && filteredEquipment.length > 0 && (
+
+
+
+ {filteredEquipment.map(([id, instance]) => {
+ const type = EQUIPMENT_TYPES[instance.typeId];
+ const Icon = type ? CATEGORY_ICONS[type.category] || Package : Package;
+ const rarityStyle = RARITY_COLORS[instance.rarity];
+
+ return (
+
+
+
+
+
+
+ {instance.name}
+
+
+ {type?.name} • {instance.usedCapacity}/{instance.totalCapacity} cap
+
+
+ {instance.rarity} • {instance.enchantments.length} enchants
+
+
+
+ {onDeleteEquipment && (
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+ {/* Delete Confirmation Dialog */}
+
setDeleteConfirm(null)}>
+
+
+
+
+ Delete Item
+
+
+ Are you sure you want to delete {deleteConfirm?.name}?
+ {deleteConfirm?.type === 'material' && (
+
+ This will delete ALL {inventory.materials[deleteConfirm?.id || ''] || 0} of this material!
+
+ )}
+ {deleteConfirm?.type === 'equipment' && (
+
+ This equipment and all its enchantments will be permanently lost!
+
+ )}
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
+ >
);
}
diff --git a/src/components/game/tabs/SpireTab.tsx b/src/components/game/tabs/SpireTab.tsx
index 058b281..f6fada5 100755
--- a/src/components/game/tabs/SpireTab.tsx
+++ b/src/components/game/tabs/SpireTab.tsx
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
-import { Swords, Sparkles, BookOpen } from 'lucide-react';
+import { Swords, Sparkles, BookOpen, ChevronUp, ChevronDown, ArrowUp, ArrowDown, RefreshCw } from 'lucide-react';
import type { GameState, GameAction } from '@/lib/game/types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
import { fmt, fmtDec, getFloorElement, calcDamage, canAffordSpellCost } from '@/lib/game/store';
@@ -20,6 +20,8 @@ interface SpireTabProps {
setSpell: (spellId: string) => void;
cancelStudy: () => void;
cancelParallelStudy: () => void;
+ setClimbDirection: (direction: 'up' | 'down') => void;
+ changeFloor: (direction: 'up' | 'down') => void;
};
upgradeEffects: ComputedEffects;
maxMana: number;
@@ -45,6 +47,11 @@ export function SpireTab({
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
const currentGuardian = GUARDIANS[store.currentFloor];
const activeSpellDef = SPELLS_DEF[store.activeSpell];
+ const climbDirection = store.climbDirection || 'up';
+ const clearedFloors = store.clearedFloors || {};
+
+ // Check if current floor is cleared (for respawn indicator)
+ const isFloorCleared = clearedFloors[store.currentFloor];
const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId];
@@ -137,6 +144,64 @@ export function SpireTab({
+ {/* Floor Navigation */}
+
+
+
Direction
+
+
+
+
+
+
+
+
+
+
+
+ {isFloorCleared && (
+
+ ⚠️ Floor will respawn when you return
+
+ )}
+
+
+
+
Best: Floor {store.maxFloorReached} •
Pacts: {store.signedPacts.length}
diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts
index 0c0acb3..131be55 100755
--- a/src/lib/game/store.ts
+++ b/src/lib/game/store.ts
@@ -2,7 +2,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
-import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect } from './types';
+import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, LootInventory } from './types';
import {
ELEMENTS,
GUARDIANS,
@@ -490,6 +490,11 @@ function makeInitial(overrides: Partial = {}): GameState {
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
+
+ // Floor Navigation
+ climbDirection: 'up',
+ clearedFloors: {},
+ lastClearedFloor: null,
spells: startSpells,
skills: overrides.skills || {},
@@ -602,6 +607,13 @@ interface GameStore extends GameState, CraftingActions, FamiliarActions {
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
tierUpSkill: (skillId: string) => void;
+ // Floor Navigation
+ setClimbDirection: (direction: 'up' | 'down') => void;
+ changeFloor: (direction: 'up' | 'down') => void;
+
+ // Inventory Management
+ updateLootInventory: (inventory: LootInventory) => void;
+
// Computed getters
getMaxMana: () => number;
getRegen: () => number;
@@ -967,6 +979,12 @@ export const useGameStore = create()(
if (floorHP <= 0) {
// Floor cleared
const wasGuardian = GUARDIANS[currentFloor];
+ const clearedFloors = state.clearedFloors;
+ const climbDirection = state.climbDirection;
+
+ // Mark this floor as cleared (needs respawn if we leave and return)
+ clearedFloors[currentFloor] = true;
+ const lastClearedFloor = currentFloor;
// ─── Loot Drop System ───
const lootDrops = rollLootDrops(currentFloor, !!wasGuardian, 0);
@@ -1003,11 +1021,22 @@ export const useGameStore = create()(
}
}
- currentFloor = currentFloor + 1;
- if (currentFloor > 100) {
- currentFloor = 100;
- }
+ // Move to next floor based on direction
+ const nextFloor = climbDirection === 'up'
+ ? Math.min(currentFloor + 1, 100)
+ : Math.max(currentFloor - 1, 1);
+
+ currentFloor = nextFloor;
floorMaxHP = getFloorMaxHP(currentFloor);
+
+ // Check if this floor was previously cleared (has enemies respawned?)
+ // Floors respawn when you leave them and come back
+ const floorWasCleared = clearedFloors[currentFloor];
+ if (floorWasCleared) {
+ // Floor has respawned - reset it but mark as uncleared
+ delete clearedFloors[currentFloor];
+ }
+
floorHP = floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
@@ -1019,6 +1048,10 @@ export const useGameStore = create()(
// Reset ALL spell progress on floor change
equipmentSpellStates = equipmentSpellStates.map(s => ({ ...s, castProgress: 0 }));
spellState = { ...spellState, castProgress: 0 };
+
+ // Update clearedFloors in the state
+ set((s) => ({ ...s, clearedFloors, lastClearedFloor }));
+
break; // Exit the while loop - new floor
}
}
@@ -1637,6 +1670,10 @@ export const useGameStore = create()(
});
},
+ updateLootInventory: (inventory: LootInventory) => {
+ set({ lootInventory: inventory });
+ },
+
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => {
const state = get();
@@ -1874,6 +1911,47 @@ export const useGameStore = create()(
if (!instance) return 0;
return instance.totalCapacity - instance.usedCapacity;
},
+
+ // ─── Floor Navigation ────────────────────────────────────────────────────────
+
+ setClimbDirection: (direction: 'up' | 'down') => {
+ set({ climbDirection: direction });
+ },
+
+ changeFloor: (direction: 'up' | 'down') => {
+ const state = get();
+ const currentFloor = state.currentFloor;
+
+ // Calculate next floor
+ const nextFloor = direction === 'up'
+ ? Math.min(currentFloor + 1, 100)
+ : Math.max(currentFloor - 1, 1);
+
+ // Can't stay on same floor
+ if (nextFloor === currentFloor) return;
+
+ // Mark current floor as cleared (it will respawn when we come back)
+ const clearedFloors = { ...state.clearedFloors };
+ clearedFloors[currentFloor] = true;
+
+ // Check if next floor was cleared (needs respawn)
+ const nextFloorCleared = clearedFloors[nextFloor];
+ if (nextFloorCleared) {
+ // Respawn the floor
+ delete clearedFloors[nextFloor];
+ }
+
+ set({
+ currentFloor: nextFloor,
+ floorMaxHP: getFloorMaxHP(nextFloor),
+ floorHP: getFloorMaxHP(nextFloor),
+ maxFloorReached: Math.max(state.maxFloorReached, nextFloor),
+ clearedFloors,
+ climbDirection: direction,
+ equipmentSpellStates: state.equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })),
+ log: [`🚶 Moved to floor ${nextFloor}${nextFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
+ });
+ },
}),
{
name: 'mana-loop-storage',
@@ -1908,6 +1986,9 @@ export const useGameStore = create()(
activeSpell: state.activeSpell,
currentAction: state.currentAction,
castProgress: state.castProgress,
+ climbDirection: state.climbDirection,
+ clearedFloors: state.clearedFloors,
+ lastClearedFloor: state.lastClearedFloor,
spells: state.spells,
skills: state.skills,
skillProgress: state.skillProgress,
@@ -1928,6 +2009,19 @@ export const useGameStore = create()(
designProgress: state.designProgress,
preparationProgress: state.preparationProgress,
applicationProgress: state.applicationProgress,
+ // Loot system
+ lootInventory: state.lootInventory,
+ lootDropsToday: state.lootDropsToday,
+ // Achievements
+ achievements: state.achievements,
+ totalDamageDealt: state.totalDamageDealt,
+ totalSpellsCast: state.totalSpellsCast,
+ totalCraftsCompleted: state.totalCraftsCompleted,
+ // Familiars
+ familiars: state.familiars,
+ activeFamiliarSlots: state.activeFamiliarSlots,
+ familiarSummonProgress: state.familiarSummonProgress,
+ totalFamiliarXpEarned: state.totalFamiliarXpEarned,
}),
}
)
diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts
index fee633e..381467b 100755
--- a/src/lib/game/types.ts
+++ b/src/lib/game/types.ts
@@ -412,6 +412,11 @@ export interface GameState {
activeSpell: string;
currentAction: GameAction;
castProgress: number; // Progress towards next spell cast (0-1)
+
+ // Floor Navigation
+ climbDirection: 'up' | 'down'; // Direction of floor traversal
+ clearedFloors: Record; // Floors that have been cleared (need respawn)
+ lastClearedFloor: number | null; // Last floor that was cleared (for respawn tracking)
// Spells
spells: Record;