From 4f4cbeb5270b22646021dfc94a1e7897366e9b2b Mon Sep 17 00:00:00 2001 From: zhipu Date: Thu, 26 Mar 2026 10:28:15 +0000 Subject: [PATCH] Add floor navigation system with up/down direction and respawn mechanics - Added climbDirection state to track player movement direction - Added clearedFloors tracking for floor respawn system - Players can now manually navigate between floors using ascend/descend buttons - Floors respawn when player leaves and returns (for loot farming) - Enhanced LootInventory component with: - Full inventory management (materials, essence, equipment) - Search and filter functionality - Sorting by name, rarity, or count - Delete functionality with confirmation dialog - Added updateLootInventory function to store - Blueprints are now shown as permanent unlocks in inventory - Floor navigation UI shows direction toggle and respawn indicators --- src/app/page.tsx | 82 +++- src/components/game/LootInventory.tsx | 515 +++++++++++++++++++++----- src/components/game/tabs/SpireTab.tsx | 67 +++- src/lib/game/store.ts | 104 +++++- src/lib/game/types.ts | 5 + 5 files changed, 679 insertions(+), 94 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index fa0d08f..cddd501 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, MAX_DAY, INCURSION_START_DAY, MANA_PER_ELEMENT, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS } from '@/lib/game/constants'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; +import { LOOT_DROPS } from '@/lib/game/data/loot-drops'; import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution'; import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import type { SkillUpgradeChoice } from '@/lib/game/types'; @@ -18,7 +19,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Sparkles, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Pause, Play, Zap, Clock, Target, Flame, Droplet, Wind, Mountain, Sun, Moon, Leaf, Skull, Brain, Link, Wind as Force, Droplets, TreeDeciduous, Hourglass, Gem, Star, CircleDot, X, Circle, BarChart3, Award, Package, Heart } from 'lucide-react'; +import { Sparkles, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Pause, Play, Zap, Clock, Target, Flame, Droplet, Wind, Mountain, Sun, Moon, Leaf, Skull, Brain, Link, Wind as Force, Droplets, TreeDeciduous, Hourglass, Gem, Star, CircleDot, X, Circle, BarChart3, Award, Package, Heart, ChevronUp, ChevronDown } from 'lucide-react'; import type { GameAction } from '@/lib/game/types'; import { CraftingTab } from '@/components/game/tabs/CraftingTab'; import { FamiliarTab } from '@/components/game/tabs/FamiliarTab'; @@ -564,6 +565,65 @@ export default function ManaLoopGame() { + {/* Floor Navigation Controls */} +
+
+ Auto-Direction +
+ + +
+
+ +
+ + +
+ + {store.clearedFloors?.[store.currentFloor] && ( +
+ + Floor will respawn when you leave and return +
+ )} +
+ + +
Best: Floor {store.maxFloorReached} • Pacts: {store.signedPacts.length} @@ -2310,7 +2370,25 @@ export default function ManaLoopGame() {
- + { + // Remove material from inventory + const newMaterials = { ...store.lootInventory.materials }; + delete newMaterials[id]; + store.updateLootInventory?.({ + ...store.lootInventory, + materials: newMaterials, + }); + store.addLog?.(`🗑️ Deleted ${LOOT_DROPS[id]?.name || id} (${amount})`); + }} + onDeleteEquipment={(instanceId) => { + store.deleteEquipmentInstance?.(instanceId); + store.addLog?.(`🗑️ Deleted equipment`); + }} + /> diff --git a/src/components/game/LootInventory.tsx b/src/components/game/LootInventory.tsx index 250ea3a..b742200 100644 --- a/src/components/game/LootInventory.tsx +++ b/src/components/game/LootInventory.tsx @@ -1,117 +1,460 @@ 'use client'; +import { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Gem, Sparkles, Scroll, Droplet } from 'lucide-react'; -import type { LootInventory as LootInventoryType } from '@/lib/game/types'; +import { Separator } from '@/components/ui/separator'; +import { Input } from '@/components/ui/input'; +import { + Gem, Sparkles, Scroll, Droplet, Trash2, Search, + Package, Sword, Shield, Shirt, Crown, ArrowUpDown, + Wrench, AlertTriangle +} from 'lucide-react'; +import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types'; import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; +import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; +import { ELEMENTS } from '@/lib/game/constants'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; interface LootInventoryProps { inventory: LootInventoryType; + elements?: Record; + equipmentInstances?: Record; + onDeleteMaterial?: (materialId: string, amount: number) => void; + onDeleteEquipment?: (instanceId: string) => void; } -export function LootInventoryDisplay({ inventory }: LootInventoryProps) { +type SortMode = 'name' | 'rarity' | 'count'; +type FilterMode = 'all' | 'materials' | 'essence' | 'blueprints' | 'equipment'; + +const RARITY_ORDER = { + common: 0, + uncommon: 1, + rare: 2, + epic: 3, + legendary: 4, + mythic: 5, +}; + +const CATEGORY_ICONS: Record = { + caster: Sword, + shield: Shield, + catalyst: Sparkles, + head: Crown, + body: Shirt, + hands: Wrench, + feet: Package, + accessory: Gem, +}; + +export function LootInventoryDisplay({ + inventory, + elements, + equipmentInstances = {}, + onDeleteMaterial, + onDeleteEquipment, +}: LootInventoryProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [sortMode, setSortMode] = useState('rarity'); + const [filterMode, setFilterMode] = useState('all'); + const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null); + + // Count items const materialCount = Object.values(inventory.materials).reduce((a, b) => a + b, 0); + const essenceCount = elements ? Object.values(elements).reduce((a, e) => a + e.current, 0) : 0; const blueprintCount = inventory.blueprints.length; - - if (materialCount === 0 && blueprintCount === 0) { + const equipmentCount = Object.keys(equipmentInstances).length; + const totalItems = materialCount + blueprintCount + equipmentCount; + + // Filter and sort materials + const filteredMaterials = Object.entries(inventory.materials) + .filter(([id, count]) => { + if (count <= 0) return false; + const drop = LOOT_DROPS[id]; + if (!drop) return false; + if (searchTerm && !drop.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; + return true; + }) + .sort(([aId, aCount], [bId, bCount]) => { + const aDrop = LOOT_DROPS[aId]; + const bDrop = LOOT_DROPS[bId]; + if (!aDrop || !bDrop) return 0; + + switch (sortMode) { + case 'name': + return aDrop.name.localeCompare(bDrop.name); + case 'rarity': + return RARITY_ORDER[bDrop.rarity] - RARITY_ORDER[aDrop.rarity]; + case 'count': + return bCount - aCount; + default: + return 0; + } + }); + + // Filter and sort essence + const filteredEssence = elements + ? Object.entries(elements) + .filter(([id, state]) => { + if (!state.unlocked || state.current <= 0) return false; + if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; + return true; + }) + .sort(([aId, aState], [bId, bState]) => { + switch (sortMode) { + case 'name': + return (ELEMENTS[aId]?.name || aId).localeCompare(ELEMENTS[bId]?.name || bId); + case 'count': + return bState.current - aState.current; + default: + return 0; + } + }) + : []; + + // Filter and sort equipment + const filteredEquipment = Object.entries(equipmentInstances) + .filter(([id, instance]) => { + if (searchTerm && !instance.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; + return true; + }) + .sort(([aId, aInst], [bId, bInst]) => { + switch (sortMode) { + case 'name': + return aInst.name.localeCompare(bInst.name); + case 'rarity': + return RARITY_ORDER[bInst.rarity] - RARITY_ORDER[aInst.rarity]; + default: + return 0; + } + }); + + // Check if we have anything to show + const hasItems = totalItems > 0 || essenceCount > 0; + + if (!hasItems) { return ( - Loot Inventory + Inventory
- No loot collected yet. Defeat floors and guardians to find items! + No items collected yet. Defeat floors and guardians to find loot!
); } - + + const handleDeleteMaterial = (materialId: string) => { + const drop = LOOT_DROPS[materialId]; + if (drop) { + setDeleteConfirm({ type: 'material', id: materialId, name: drop.name }); + } + }; + + const handleDeleteEquipment = (instanceId: string) => { + const instance = equipmentInstances[instanceId]; + if (instance) { + setDeleteConfirm({ type: 'equipment', id: instanceId, name: instance.name }); + } + }; + + const confirmDelete = () => { + if (!deleteConfirm) return; + + if (deleteConfirm.type === 'material' && onDeleteMaterial) { + onDeleteMaterial(deleteConfirm.id, inventory.materials[deleteConfirm.id] || 0); + } else if (deleteConfirm.type === 'equipment' && onDeleteEquipment) { + onDeleteEquipment(deleteConfirm.id); + } + + setDeleteConfirm(null); + }; + return ( - - - - - Loot Inventory - - {materialCount + blueprintCount} items - - - - - -
- {/* 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 && ( +
+
+ + Equipment +
+
+ {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;