Compare commits
7 Commits
984459200b
...
454195cdfb
| Author | SHA1 | Date | |
|---|---|---|---|
| 454195cdfb | |||
| 88d6016557 | |||
| 1e5eae9b9d | |||
| c8a01acda3 | |||
| 351b6c2dca | |||
| 6f0b86d4d7 | |||
| b0a254b481 |
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
|
||||||
|
|
||||||
|
if echo "$changed_files" | grep --quiet -E "package.json|package-lock.json"; then
|
||||||
|
echo "📦 Dependencies changed. Syncing..."
|
||||||
|
|
||||||
|
# --no-progress stops the terminal spam
|
||||||
|
# --loglevel error ensures we only see the bad stuff
|
||||||
|
if npm install --no-progress --loglevel error; then
|
||||||
|
echo "✅ Node modules are up to date."
|
||||||
|
else
|
||||||
|
echo "❌ npm install failed! Please check your connection or package.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "🔍 Running pre-commit checks..."
|
||||||
|
|
||||||
|
# Get staged files (added, copied, modified)
|
||||||
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||||
|
|
||||||
|
if [ -n "$STAGED_FILES" ]; then
|
||||||
|
echo "📏 Checking file sizes..."
|
||||||
|
node .husky/scripts/check-file-size.js $STAGED_FILES
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate project structure
|
||||||
|
echo "🗺️ Updating project structure..."
|
||||||
|
node .husky/scripts/generate-project-tree.js
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-add the generated project structure to the commit
|
||||||
|
git add docs/project-structure.txt
|
||||||
|
|
||||||
|
echo "✅ All pre-commit checks passed!"
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const MAX_LINES = 400;
|
||||||
|
|
||||||
|
// List of file patterns to ignore (optional, can be customized)
|
||||||
|
const IGNORE_PATTERNS = [
|
||||||
|
/\.lock$/, // Lock files
|
||||||
|
/\.min\.js$/, // Minified files
|
||||||
|
/\.map$/, // Source maps
|
||||||
|
/package-lock\.json$/,
|
||||||
|
/bun\.lock$/,
|
||||||
|
/tsconfig\.tsbuildinfo$/,
|
||||||
|
/\.md$/, // Markdown documentation files
|
||||||
|
/context\.md$/, // Context files for sub-agents
|
||||||
|
/project-structure\.txt$/, // Generated project structure
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldIgnore(filePath) {
|
||||||
|
return IGNORE_PATTERNS.some(pattern => pattern.test(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = process.argv.slice(2);
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('ℹ️ No files to check');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Skip ignored patterns
|
||||||
|
if (shouldIgnore(file)) {
|
||||||
|
console.log(`⏭️ Skipping ${file} (ignored pattern)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists (it might have been deleted)
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
console.log(`⏭️ Skipping ${file} (file does not exist)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n').length;
|
||||||
|
|
||||||
|
if (lines > MAX_LINES) {
|
||||||
|
console.error(`❌ ${file} is too large (${lines} lines, max ${MAX_LINES}). AI agents will struggle. Please refactor!`);
|
||||||
|
hasError = true;
|
||||||
|
} else {
|
||||||
|
console.log(`✅ ${file} (${lines} lines) - OK`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`⚠️ Error reading ${file}: ${err.message}`);
|
||||||
|
// Don't fail on read errors, just warn
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
// Directory to start from (project root)
|
||||||
|
const ROOT_DIR = process.cwd();
|
||||||
|
// Output file path
|
||||||
|
const OUTPUT_FILE = path.join(ROOT_DIR, 'docs', 'project-structure.txt');
|
||||||
|
|
||||||
|
// Function to check if a path is ignored by git
|
||||||
|
function isGitIgnored(filePath) {
|
||||||
|
try {
|
||||||
|
// git check-ignore -q returns 0 if ignored, 1 if not
|
||||||
|
execSync(`git check-ignore -q "${filePath}"`, {
|
||||||
|
cwd: ROOT_DIR,
|
||||||
|
stdio: 'ignore'
|
||||||
|
});
|
||||||
|
return true; // Ignored
|
||||||
|
} catch (e) {
|
||||||
|
return false; // Not ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate tree structure
|
||||||
|
function generateTree(dir, prefix = '', isRoot = true) {
|
||||||
|
let structure = '';
|
||||||
|
|
||||||
|
// Add root directory name if it's the root
|
||||||
|
if (isRoot) {
|
||||||
|
structure += `${path.basename(dir)}/\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items;
|
||||||
|
try {
|
||||||
|
items = fs.readdirSync(dir);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error reading directory ${dir}: ${e.message}`);
|
||||||
|
return structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort items: directories first, then files
|
||||||
|
const dirs = [];
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
|
||||||
|
// Skip if ignored by git
|
||||||
|
if (isGitIgnored(itemPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(itemPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
dirs.push(item);
|
||||||
|
} else {
|
||||||
|
files.push(item);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Skip items we can't stat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort directories and files alphabetically
|
||||||
|
dirs.sort();
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
const allItems = [...dirs, ...files];
|
||||||
|
|
||||||
|
allItems.forEach((item, index) => {
|
||||||
|
const isLast = index === allItems.length - 1;
|
||||||
|
const connector = isLast ? '└── ' : '├── ';
|
||||||
|
const itemPath = path.join(dir, item);
|
||||||
|
|
||||||
|
structure += `${prefix}${connector}${item}${dirs.includes(item) ? '/' : ''}\n`;
|
||||||
|
|
||||||
|
// Recurse into directories
|
||||||
|
if (dirs.includes(item)) {
|
||||||
|
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
||||||
|
structure += generateTree(itemPath, newPrefix, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🗺️ Generating project structure...');
|
||||||
|
|
||||||
|
// Ensure docs directory exists
|
||||||
|
const docsDir = path.join(ROOT_DIR, 'docs');
|
||||||
|
if (!fs.existsSync(docsDir)) {
|
||||||
|
fs.mkdirSync(docsDir, { recursive: true });
|
||||||
|
console.log('📁 Created docs directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tree
|
||||||
|
const tree = generateTree(ROOT_DIR, '', true);
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
fs.writeFileSync(OUTPUT_FILE, tree);
|
||||||
|
console.log(`✅ Project structure updated: ${OUTPUT_FILE}`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Error generating project structure: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -120,6 +120,8 @@ src/
|
|||||||
└── utils.ts # General utilities (cn function)
|
└── utils.ts # General utilities (cn function)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*Note: A complete, up-to-date project tree is automatically generated on each commit and saved to `docs/project-structure.txt`. This file is generated by the pre-commit hook using `.husky/scripts/generate-project-tree.js` and respects `.gitignore` rules.*
|
||||||
|
|
||||||
## Key Systems
|
## Key Systems
|
||||||
|
|
||||||
### 0. Task 2 Completion Summary
|
### 0. Task 2 Completion Summary
|
||||||
@@ -304,6 +306,27 @@ damage *= effects.myNewStatMultiplier;
|
|||||||
3. **Add research skill in `constants.ts`**
|
3. **Add research skill in `constants.ts`**
|
||||||
4. **Map research to effect in `EFFECT_RESEARCH_MAPPING`**
|
4. **Map research to effect in `EFFECT_RESEARCH_MAPPING`**
|
||||||
|
|
||||||
|
## Git Hooks (Husky)
|
||||||
|
|
||||||
|
This project uses **Husky** to manage git hooks for automated checks and agent assistance:
|
||||||
|
|
||||||
|
### Pre-Commit Hook (`.husky/pre-commit`)
|
||||||
|
Runs automatically before each commit:
|
||||||
|
1. **File Size Check**: Ensures no staged file exceeds 400 lines (improves AI agent readability)
|
||||||
|
2. **Project Structure Generation**: Updates `docs/project-structure.txt` with current tree (respects `.gitignore`)
|
||||||
|
|
||||||
|
### Post-Merge Hook (`.husky/post-merge`)
|
||||||
|
Runs after merging branches:
|
||||||
|
- Checks if `package.json` or `package-lock.json` changed
|
||||||
|
- Automatically runs `npm install` to sync dependencies
|
||||||
|
|
||||||
|
### Implementation Files
|
||||||
|
- Hook scripts: `.husky/` directory
|
||||||
|
- File size check: `.husky/scripts/check-file-size.js`
|
||||||
|
- Tree generator: `.husky/scripts/generate-project-tree.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
1. **Forgetting to call `getUnifiedEffects()`**: Always use unified effects for stat calculations
|
1. **Forgetting to call `getUnifiedEffects()`**: Always use unified effects for stat calculations
|
||||||
@@ -402,6 +425,9 @@ const useGameStore = create<GameStore>()(
|
|||||||
- **After Task 2**: `page.tsx` reduced from ~2554 to ~548 lines (78% reduction)
|
- **After Task 2**: `page.tsx` reduced from ~2554 to ~548 lines (78% reduction)
|
||||||
- **After Task 2**: `store.ts` increased due to crafting-slice integration, but better organized
|
- **After Task 2**: `store.ts` increased due to crafting-slice integration, but better organized
|
||||||
|
|
||||||
|
### Automated File Size Check
|
||||||
|
A pre-commit hook automatically checks all staged files. Files exceeding **400 lines** will be rejected. The hook runs via Husky and uses `.husky/scripts/check-file-size.js`. If your file is too large, refactor it into smaller modules before committing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚫 BANNED CONTENT - NEVER ADD THESE
|
## 🚫 BANNED CONTENT - NEVER ADD THESE
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+28
-27
@@ -2,69 +2,70 @@
|
|||||||
|
|
||||||
## Status Overview
|
## Status Overview
|
||||||
- **Start Date**: 2025-05-19
|
- **Start Date**: 2025-05-19
|
||||||
- **Current Phase**: PRIORITY 2 (Spire Mode Fixes)
|
- **Current Phase**: PRIORITY 3 (UI/UX Restructuring)
|
||||||
- **Overall Progress**: 21% complete (4/19 tasks done)
|
- **Overall Progress**: 42% complete (8/19 tasks done)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 0 — Crashes (Fix First, Parallel) ✅ COMPLETED
|
## PRIORITY 0 — Crashes (Fix First, Parallel) ✅ COMPLETED
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| SpellsTab crash diagnosis/fix | Completed | Fixed unprotected ENCHANTMENT_EFFECTS access, added spell.cost guards |
|
| SpellsTab crash diagnosis/fix | Completed | Fixed unprotected ENCHANTMENT_EFFECTS access |
|
||||||
| LabTab crash diagnosis/fix | Completed | Added safe access to store.elements with `|| {}` fallbacks |
|
| LabTab crash diagnosis/fix | Completed | Added safe access to store.elements |
|
||||||
| DebugTab crash diagnosis/fix | Completed | Moved Toaster/GameToaster inside DebugProvider in layout.tsx |
|
| DebugTab crash diagnosis/fix | Completed | Moved Toaster/GameToaster inside DebugProvider |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 1 — Mana Conversion Mechanic Fix ✅ COMPLETED
|
## PRIORITY 1 — Mana Conversion Mechanic Fix ✅ COMPLETED
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| Wire conversion drain to effectiveRegen instead of active mana pool | Completed | Removed redundant `rawMana -= actualConversion` in store.ts since effectiveRegen already accounts for conversion drain |
|
| Wire conversion drain to effectiveRegen | Completed | Removed redundant rawMana -= actualConversion |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 2 — Spire Mode Fixes
|
## PRIORITY 2 — Spire Mode Fixes
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| 2a. Floor Rendering & Identity (type, named enemy, special properties) | Pending | |
|
| 2a. Floor Rendering & Identity | Pending | Context gathering next |
|
||||||
| 2b. Swarm Floors (show multiple enemies, verify generation) | Pending | |
|
| 2b. Swarm Floors | ✅ Completed | Verified by check sub-agent |
|
||||||
| 2c. HP Bar Live Updates | Pending | |
|
| 2c. HP Bar Live Updates | ✅ Completed | floorHP synced to enemy HP |
|
||||||
| 2d. Casting Progress Overflow Fix | Pending | |
|
| 2d. Casting Progress Overflow | Pending | Context gathering next |
|
||||||
| 2e. Climb/Descend Controls (spam fix, re-entry resume, button rename) | Pending | |
|
| 2e. Climb/Descend Controls | Pending | Context gathering next |
|
||||||
| 2f. Activity Log Implementation | Pending | |
|
| 2f. Activity Log Implementation | Pending | Context gathering next |
|
||||||
| 2g. Spell Info Display Fix (dmg/cast vs DPS) | Pending | |
|
| 2g. Spell Info Display Fix | Pending | Context gathering next |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 3 — UI/UX Restructuring
|
## PRIORITY 3 — UI/UX Restructuring
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| 3a. CraftingTab Restructure (remove 1-4 bar, split Fabricate/Enchant, top sub-tabs) | Pending | |
|
| 3a. CraftingTab Restructure | ✅ Completed | Removed stepper, added Fabricate/Enchant tabs |
|
||||||
| 3b. LootTab Nesting Fix (remove redundant layers) | Pending | |
|
| 3b. LootTab Nesting Fix | ✅ Completed | Removed redundant LootTab wrapper |
|
||||||
| 3c. AchievementsTab Nesting Fix (remove duplicate headings) | Pending | |
|
| 3c. AchievementsTab Nesting Fix | In Progress | Context gathering → execution |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 4 — Enchantment Effects & Research
|
## PRIORITY 4 — Enchantment Effects & Research
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| 4a. Mana-Type Capacity Enchantment Effects | Pending | Per unlocked mana type |
|
| 4a. Mana-Type Capacity Enchantment Effects | Pending | Context gathering next |
|
||||||
| 4b. Mana Capacity Research Visibility Gate | Pending | Only show if mana type unlocked |
|
| 4b. Mana Capacity Research Visibility Gate | Pending | Context gathering next |
|
||||||
| 4c. Skill Requirement Display Bug Fix (undefined Lv.[object Object]) | Pending | |
|
| 4c. Skill Requirement Display Bug Fix | Pending | Context gathering next |
|
||||||
| 4d. Enchantment Power Effect Implementation + Stub Audit | Pending | Replace placeholder, audit all stubs |
|
| 4d. Enchantment Power Effect Implementation | Pending | Partially done |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRIORITY 5 — Insight Upgrade Analysis
|
## PRIORITY 5 — Insight Upgrade Analysis
|
||||||
| Task | Status | Notes |
|
| Task | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| 5a. Create design proposal in docs/task5_insight_proposals.md | Pending | Wait for human sign-off |
|
| 5a. Create design proposal | Pending | Context gathering next |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes & Decisions
|
## Workflow Log
|
||||||
- ✅ PRIORITY 0 crashes fixed via parallel sub-agents, verified and applied all fixes
|
- ✅ PRIORITY 0 crashes fixed via parallel sub-agents
|
||||||
- ✅ PRIORITY 1 mana conversion fix applied: removed double-counting of conversion drain in store.ts tick logic
|
- ✅ PRIORITY 1 mana conversion fix applied
|
||||||
- Next steps: Dispatch parallel sub-agents for PRIORITY 2 Spire Mode fixes (2a-2g)
|
- ✅ PRIORITY 2b, 2c verified completed
|
||||||
- Advisor tool will be used for ambiguous design decisions
|
- ✅ Task 12 (CraftingTab) completed
|
||||||
- Sub-agent instructions passed via inline prompts with full context
|
- ✅ Task 13 (LootTab) completed
|
||||||
|
- ⏳ Current: Task 14 (AchievementsTab) context gathering
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
# Task 12 Context: Restructure CraftingTab
|
||||||
|
|
||||||
|
## Task Description
|
||||||
|
Restructure CraftingTab (remove 1-4 progress bar, split Fabricate/Enchant, top sub-tabs) (PRIORITY 3a)
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
|
||||||
|
### 1. `/src/components/game/tabs/CraftingTab.tsx` (268 lines)
|
||||||
|
|
||||||
|
**Imports and Dependencies:**
|
||||||
|
```typescript
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { Stepper } from '@/components/ui/stepper';
|
||||||
|
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
||||||
|
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||||
|
import { fmt, type GameStore } from '@/lib/game/store';
|
||||||
|
import {
|
||||||
|
EnchantmentDesigner,
|
||||||
|
EnchantmentPreparer,
|
||||||
|
EnchantmentApplier,
|
||||||
|
EquipmentCrafter,
|
||||||
|
} from '@/components/game/crafting';
|
||||||
|
import { useGameToast } from '@/components/game/GameToast';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Crafting Phases Constant:**
|
||||||
|
```typescript
|
||||||
|
// Crafting phases for the stepper
|
||||||
|
const CRAFTING_PHASES = ['Design', 'Prepare', 'Apply', 'Craft'];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component Props:**
|
||||||
|
```typescript
|
||||||
|
export interface CraftingTabProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**State and Stepper Mapping:**
|
||||||
|
```typescript
|
||||||
|
export function CraftingTab({ store }: CraftingTabProps) {
|
||||||
|
const showToast = useGameToast();
|
||||||
|
const currentAction = store.currentAction;
|
||||||
|
const designProgress = store.designProgress;
|
||||||
|
const preparationProgress = store.preparationProgress;
|
||||||
|
const applicationProgress = store.applicationProgress;
|
||||||
|
const equipmentCraftingProgress = store.equipmentCraftingProgress;
|
||||||
|
const pauseApplication = store.pauseApplication;
|
||||||
|
const resumeApplication = store.resumeApplication;
|
||||||
|
|
||||||
|
const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft');
|
||||||
|
|
||||||
|
// Map crafting stage to stepper index
|
||||||
|
const getStepperIndex = (stage: string): number => {
|
||||||
|
switch (stage) {
|
||||||
|
case 'design': return 0;
|
||||||
|
case 'prepare': return 1;
|
||||||
|
case 'apply': return 2;
|
||||||
|
case 'craft': return 3;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stepper Component (lines 58-68):**
|
||||||
|
```tsx
|
||||||
|
{/* Visual Stepper - Requirement: show Design, Prepare, Apply phases as visual stepper */}
|
||||||
|
<GameCard variant="default" className="p-4">
|
||||||
|
<Stepper
|
||||||
|
steps={CRAFTING_PHASES}
|
||||||
|
currentStep={getStepperIndex(craftingStage)}
|
||||||
|
className="px-4"
|
||||||
|
/>
|
||||||
|
</GameCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stage Content Conditional Rendering (lines 71-97):**
|
||||||
|
```tsx
|
||||||
|
{/* Stage Content - Without unlabeled Tabs, using conditional rendering instead */}
|
||||||
|
<div className="mt-4">
|
||||||
|
{craftingStage === 'craft' && (
|
||||||
|
<EquipmentCrafter store={store} />
|
||||||
|
)}
|
||||||
|
{craftingStage === 'design' && (
|
||||||
|
<EnchantmentDesigner
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentType={null}
|
||||||
|
setSelectedEquipmentType={() => {}}
|
||||||
|
selectedEffects={[]}
|
||||||
|
setSelectedEffects={() => {}}
|
||||||
|
designName={''}
|
||||||
|
setDesignName={() => {}}
|
||||||
|
selectedDesign={null}
|
||||||
|
setSelectedDesign={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{craftingStage === 'prepare' && (
|
||||||
|
<EnchantmentPreparer
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentInstance={null}
|
||||||
|
setSelectedEquipmentInstance={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{craftingStage === 'apply' && (
|
||||||
|
<EnchantmentApplier
|
||||||
|
store={store}
|
||||||
|
selectedEquipmentInstance={null}
|
||||||
|
setSelectedEquipmentInstance={() => {}}
|
||||||
|
selectedDesign={null}
|
||||||
|
setSelectedDesign={() => {}}
|
||||||
|
onEnchantmentApplied={handleEnchantmentApplied}
|
||||||
|
onCapacityExceeded={handleCapacityExceeded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stage Navigation Buttons (lines 99-131):**
|
||||||
|
```tsx
|
||||||
|
{/* Stage Navigation Buttons */}
|
||||||
|
<GameCard variant="default" className="p-4">
|
||||||
|
<div className="flex justify-center gap-2 flex-wrap">
|
||||||
|
<ActionButton
|
||||||
|
variant={craftingStage === 'craft' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCraftingStage('craft')}
|
||||||
|
className={craftingStage === 'craft' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Anvil size={14} className="mr-1" />
|
||||||
|
Craft
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={craftingStage === 'design' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCraftingStage('design')}
|
||||||
|
className={craftingStage === 'design' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Scroll size={14} className="mr-1" />
|
||||||
|
Design
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={craftingStage === 'prepare' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCraftingStage('prepare')}
|
||||||
|
className={craftingStage === 'prepare' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Hammer size={14} className="mr-1" />
|
||||||
|
Prepare
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={craftingStage === 'apply' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCraftingStage('apply')}
|
||||||
|
className={craftingStage === 'apply' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Sparkles size={14} className="mr-1" />
|
||||||
|
Apply
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</GameCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Activity Indicators (Progress Bars to be removed - lines 133-236):**
|
||||||
|
```tsx
|
||||||
|
{/* Current Activity Indicator */}
|
||||||
|
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||||
|
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
||||||
|
<SectionHeader
|
||||||
|
title="Crafting Equipment"
|
||||||
|
action={
|
||||||
|
<span className="text-sm text-[var(--text-muted)]">
|
||||||
|
{safeToFixed(calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required), 0)}%
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Progress
|
||||||
|
value={calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required)}
|
||||||
|
className="h-3 bg-[var(--bg-sunken)]"
|
||||||
|
/>
|
||||||
|
{/* ... more content ... */}
|
||||||
|
</GameCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentAction === 'design' && designProgress && (
|
||||||
|
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
||||||
|
{/* ... Progress bar and content ... */}
|
||||||
|
</GameCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentAction === 'prepare' && preparationProgress && (
|
||||||
|
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
||||||
|
{/* ... Progress bar and content ... */}
|
||||||
|
</GameCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentAction === 'enchant' && applicationProgress && (
|
||||||
|
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
||||||
|
{/* ... Progress bar and content ... */}
|
||||||
|
</GameCard>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Files in `/src/components/game/crafting/` Directory
|
||||||
|
|
||||||
|
| File | Size | Last Modified |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `EnchantmentApplier.tsx` | 12,206 bytes | 1777364523 |
|
||||||
|
| `EnchantmentDesigner.tsx` | 19,568 bytes | 1777361558 |
|
||||||
|
| `EnchantmentPreparer.tsx` | 14,816 bytes | 1777365343 |
|
||||||
|
| `EquipmentCrafter.tsx` | 9,121 bytes | 1777205526 |
|
||||||
|
| `index.tsx` | 396 bytes | 1777028644 |
|
||||||
|
|
||||||
|
**Barrel File (`index.tsx`):**
|
||||||
|
```typescript
|
||||||
|
// Barrel file for crafting components
|
||||||
|
|
||||||
|
export { EnchantmentDesigner, type EnchantmentDesignerProps } from './EnchantmentDesigner';
|
||||||
|
export { EnchantmentPreparer, type EnchantmentPreparerProps } from './EnchantmentPreparer';
|
||||||
|
export { EnchantmentApplier, type EnchantmentApplierProps } from './EnchantmentApplier';
|
||||||
|
export { EquipmentCrafter, type EquipmentCrafterProps } from './EquipmentCrafter';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Stepper Component (`/src/components/ui/stepper.tsx`)
|
||||||
|
|
||||||
|
**Interface:**
|
||||||
|
```typescript
|
||||||
|
interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
steps: string[];
|
||||||
|
currentStep: number; // 0-indexed
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full Implementation (100 lines):**
|
||||||
|
```typescript
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Check, Circle, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
steps: string[];
|
||||||
|
currentStep: number; // 0-indexed
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepProps {
|
||||||
|
label: string;
|
||||||
|
stepNumber: number;
|
||||||
|
isActive: boolean;
|
||||||
|
isCompleted: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step = ({ label, stepNumber, isActive, isCompleted, isLast, orientation = "horizontal" }: StepProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center",
|
||||||
|
orientation === "vertical" ? "flex-col" : "flex-row",
|
||||||
|
orientation === "vertical" && "w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center w-8 h-8 rounded-full border-2 transition-all duration-200",
|
||||||
|
isActive && "border-[var(--interactive-primary)] bg-[var(--interactive-primary)]/20 text-[var(--interactive-primary)]",
|
||||||
|
isCompleted && "border-[var(--color-success)] bg-[var(--color-success)]/20 text-[var(--color-success)]",
|
||||||
|
!isActive && !isCompleted && "border-[var(--border-default)] bg-[var(--bg-sunken)] text-[var(--text-muted)]"
|
||||||
|
)}
|
||||||
|
aria-current={isActive ? "step" : undefined}
|
||||||
|
>
|
||||||
|
{isCompleted ? (
|
||||||
|
<Check size={16} />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-semibold">{stepNumber}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-xs mt-1 font-medium",
|
||||||
|
isActive && "text-[var(--interactive-primary)]",
|
||||||
|
isCompleted && "text-[var(--color-success)]",
|
||||||
|
!isActive && !isCompleted && "text-[var(--text-muted)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!isLast && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 mx-2",
|
||||||
|
orientation === "vertical" ? "h-8 w-px my-1" : "h-px",
|
||||||
|
isCompleted ? "bg-[var(--color-success)]" : "bg-[var(--border-default)]"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Stepper({ steps, currentStep, orientation = "horizontal", className, ...props }: StepperProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="stepper"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full",
|
||||||
|
orientation === "horizontal" ? "flex-row items-center" : "flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="list"
|
||||||
|
aria-label="Progress steps"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={step}
|
||||||
|
className={cn("flex items-center", orientation === "vertical" && "w-full")}
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<Step
|
||||||
|
label={step}
|
||||||
|
stepNumber={index + 1}
|
||||||
|
isActive={index === currentStep}
|
||||||
|
isCompleted={index < currentStep}
|
||||||
|
isLast={index === steps.length - 1}
|
||||||
|
orientation={orientation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Current Sub-Tab/Navigation Implementation Details
|
||||||
|
|
||||||
|
**Current Structure:**
|
||||||
|
The CraftingTab currently uses a **4-stage linear workflow** with:
|
||||||
|
1. A visual Stepper component showing phases: Design → Prepare → Apply → Craft
|
||||||
|
2. Navigation buttons at the bottom to switch between stages
|
||||||
|
3. Conditional rendering of content based on `craftingStage` state
|
||||||
|
|
||||||
|
**Current Stages:**
|
||||||
|
- `design` - EnchantmentDesigner component (Design enchantments)
|
||||||
|
- `prepare` - EnchantmentPreparer component (Prepare equipment)
|
||||||
|
- `apply` - EnchantmentApplier component (Apply enchantments)
|
||||||
|
- `craft` - EquipmentCrafter component (Craft equipment)
|
||||||
|
|
||||||
|
**Issues to Address (Task Requirements):**
|
||||||
|
1. **Remove 1-4 progress bar** - The Stepper component (lines 58-68) needs to be removed
|
||||||
|
2. **Split Fabricate/Enchant** - Currently "Craft" (EquipmentCrafter) is mixed in with enchantment workflow. Need to split into:
|
||||||
|
- "Fabricate" tab - for EquipmentCrafter (crafting equipment)
|
||||||
|
- "Enchant" tab - for the Design → Prepare → Apply workflow
|
||||||
|
3. **Top sub-tabs** - Replace the bottom navigation buttons with proper top-level sub-tabs
|
||||||
|
|
||||||
|
**Current Navigation Pattern:**
|
||||||
|
- State: `craftingStage` (useState with 4 possible values)
|
||||||
|
- Navigation: 4 ActionButtons at the bottom of the tab
|
||||||
|
- Visual indicator: Stepper at the top showing progress through phases
|
||||||
|
|
||||||
|
**Suggested New Structure (for implementation):**
|
||||||
|
```
|
||||||
|
CraftingTab
|
||||||
|
├── Top Sub-Tabs: [Fabricate] [Enchant]
|
||||||
|
├── Fabricate Content: EquipmentCrafter
|
||||||
|
└── Enchant Content:
|
||||||
|
├── Sub-Navigation: [Design] [Prepare] [Apply]
|
||||||
|
├── Design: EnchantmentDesigner
|
||||||
|
├── Prepare: EnchantmentPreparer
|
||||||
|
└── Apply: EnchantmentApplier
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Component Props Signatures
|
||||||
|
|
||||||
|
**EquipmentCrafterProps:**
|
||||||
|
```typescript
|
||||||
|
export interface EquipmentCrafterProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**EnchantmentDesignerProps:**
|
||||||
|
```typescript
|
||||||
|
export interface EnchantmentDesignerProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentType: string | null;
|
||||||
|
setSelectedEquipmentType: (type: string | null) => void;
|
||||||
|
selectedEffects: DesignEffect[];
|
||||||
|
setSelectedEffects: (effects: DesignEffect[]) => void;
|
||||||
|
designName: string;
|
||||||
|
setDesignName: (name: string) => void;
|
||||||
|
selectedDesign: string | null;
|
||||||
|
setSelectedDesign: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**EnchantmentPreparerProps:**
|
||||||
|
```typescript
|
||||||
|
export interface EnchantmentPreparerProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentInstance: string | null;
|
||||||
|
setSelectedEquipmentInstance: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**EnchantmentApplierProps:**
|
||||||
|
```typescript
|
||||||
|
export interface EnchantmentApplierProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentInstance: string | null;
|
||||||
|
setSelectedEquipmentInstance: (id: string | null) => void;
|
||||||
|
selectedDesign: string | null;
|
||||||
|
setSelectedDesign: (id: string | null) => void;
|
||||||
|
onEnchantmentApplied?: () => void;
|
||||||
|
onCapacityExceeded?: (itemName: string, used: number, total: number) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Key Observations for Restructuring
|
||||||
|
|
||||||
|
1. **Stepper Removal**: The `CRAFTING_PHASES` constant and `Stepper` component usage must be removed from CraftingTab
|
||||||
|
|
||||||
|
2. **State Management**: The `craftingStage` state will need to be replaced with:
|
||||||
|
- A top-level tab state (`fabricate` | `enchant`)
|
||||||
|
- An enchant sub-stage state (`design` | `prepare` | `apply`) when in enchant mode
|
||||||
|
|
||||||
|
3. **Progress Bars**: The activity indicators with Progress components (lines 133-236) should potentially be moved into their respective components (EquipmentCrafter, EnchantmentDesigner, etc.) rather than being in CraftingTab
|
||||||
|
|
||||||
|
4. **No Tab Component**: Currently, the app doesn't use a Tab component - it uses conditional rendering with ActionButtons. The restructured version should implement proper tabs at the top level
|
||||||
|
|
||||||
|
5. **Helper Functions**: The `safeToFixed` and `calcPercent` helpers are used for progress bars - these may no longer be needed in CraftingTab after restructuring
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
# Task 13 Context: Fix LootTab Nesting (Remove Redundant Layers)
|
||||||
|
|
||||||
|
## Task Description
|
||||||
|
Fix LootTab nesting (remove redundant layers) (PRIORITY 3b)
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
|
||||||
|
### 1. `/src/components/game/tabs/LootTab.tsx` (48 lines)
|
||||||
|
|
||||||
|
**Full Content:**
|
||||||
|
```typescript
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||||
|
|
||||||
|
export interface LootTabProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LootTab({ store }: LootTabProps) {
|
||||||
|
const inventory = store.lootInventory;
|
||||||
|
const elements = store.elements;
|
||||||
|
const equipmentInstances = store.equipmentInstances;
|
||||||
|
|
||||||
|
// Count items for badge
|
||||||
|
const materialCount = Object.values(inventory.materials).reduce((a, b) => a + b, 0);
|
||||||
|
const blueprintCount = inventory.blueprints.length;
|
||||||
|
const equipmentCount = Object.keys(equipmentInstances).length;
|
||||||
|
const totalItems = materialCount + blueprintCount + equipmentCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||||
|
💎 Loot Inventory
|
||||||
|
<Badge className="ml-auto bg-gray-800 text-gray-300">
|
||||||
|
{totalItems} items
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<LootInventoryDisplay
|
||||||
|
inventory={inventory}
|
||||||
|
elements={elements}
|
||||||
|
equipmentInstances={equipmentInstances}
|
||||||
|
onDeleteMaterial={store.deleteMaterial}
|
||||||
|
onDeleteEquipment={store.deleteEquipmentInstance}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LootTab.displayName = "LootTab";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Observations - LootTab Redundant Wrapper:**
|
||||||
|
- Uses `Card` component from `@/components/ui/card` with header "💎 Loot Inventory"
|
||||||
|
- Shows a badge with total items count
|
||||||
|
- Wraps `LootInventoryDisplay` inside `CardContent`
|
||||||
|
- **This creates the outer layer of nesting**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `/src/components/game/LootInventory.tsx` (499 lines)
|
||||||
|
|
||||||
|
**Component Interface:**
|
||||||
|
```typescript
|
||||||
|
interface LootInventoryProps {
|
||||||
|
inventory: LootInventoryType;
|
||||||
|
elements?: Record<string, ElementState>;
|
||||||
|
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||||
|
onDeleteMaterial?: (materialId: string, amount: number) => void;
|
||||||
|
onDeleteEquipment?: (instanceId: string) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Main Component Export:**
|
||||||
|
```typescript
|
||||||
|
export function LootInventoryDisplay({
|
||||||
|
inventory,
|
||||||
|
elements,
|
||||||
|
equipmentInstances = {},
|
||||||
|
onDeleteMaterial,
|
||||||
|
onDeleteEquipment,
|
||||||
|
}: LootInventoryProps) {
|
||||||
|
// ... state and handlers ...
|
||||||
|
|
||||||
|
// Check if we have anything to show
|
||||||
|
const hasItems = totalItems > 0 || essenceCount > 0;
|
||||||
|
|
||||||
|
if (!hasItems) {
|
||||||
|
return (
|
||||||
|
<GameCard variant="default" className="w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-[var(--text-muted)] text-sm text-center py-4">
|
||||||
|
No items collected yet. Defeat floors and guardians to find loot!
|
||||||
|
</div>
|
||||||
|
</GameCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... handlers ...
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GameCard variant="default" className="w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
className="ml-auto bg-[var(--bg-sunken)] text-[var(--text-secondary)] text-xs border-[var(--border-subtle)]"
|
||||||
|
aria-label={`${totalItems} items in inventory`}
|
||||||
|
>
|
||||||
|
{totalItems} items
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filter Controls */}
|
||||||
|
{/* ... */}
|
||||||
|
|
||||||
|
{/* Filter Tabs */}
|
||||||
|
{/* ... */}
|
||||||
|
|
||||||
|
<Separator className="bg-[var(--border-subtle)] mb-3" />
|
||||||
|
|
||||||
|
<ScrollArea className="h-64 w-full">
|
||||||
|
{/* Materials, Essence, Blueprints, Equipment sections */}
|
||||||
|
{/* ... */}
|
||||||
|
</ScrollArea>
|
||||||
|
</GameCard>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
||||||
|
{/* ... */}
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Observations - LootInventory Redundant Wrapper:**
|
||||||
|
- Uses `GameCard` component (from `@/components/ui/game-card`)
|
||||||
|
- Has its own header with "Inventory" title and `Gem` icon
|
||||||
|
- Shows a badge with total items count (duplicating LootTab's badge)
|
||||||
|
- Contains all the actual content: search, filters, items display
|
||||||
|
- **This creates the inner layer of nesting**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Duplicate Headings/Wrappers Issue
|
||||||
|
|
||||||
|
### Redundant Card Nesting:
|
||||||
|
```
|
||||||
|
LootTab (Outer Card)
|
||||||
|
├── CardHeader: "💎 Loot Inventory" + Badge: "{totalItems} items"
|
||||||
|
└── CardContent
|
||||||
|
└── LootInventoryDisplay (Inner GameCard)
|
||||||
|
├── Header: "Inventory" + Badge: "{totalItems} items" ← DUPLICATE
|
||||||
|
├── Search/Filter Controls
|
||||||
|
├── Items Display
|
||||||
|
└── Delete Dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Code Duplication:
|
||||||
|
|
||||||
|
**LootTab.tsx (lines 24-33) - Outer Header:**
|
||||||
|
```tsx
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||||
|
💎 Loot Inventory
|
||||||
|
<Badge className="ml-auto bg-gray-800 text-gray-300">
|
||||||
|
{totalItems} items
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
```
|
||||||
|
|
||||||
|
**LootInventory.tsx (lines 191-202) - Inner Header (DUPLICATE):**
|
||||||
|
```tsx
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
className="ml-auto bg-[var(--bg-sunken)] text-[var(--text-secondary)] text-xs border-[var(--border-subtle)]"
|
||||||
|
aria-label={`${totalItems} items in inventory`}
|
||||||
|
>
|
||||||
|
{totalItems} items
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redundant Badge Count:
|
||||||
|
- LootTab shows: `{totalItems} items`
|
||||||
|
- LootInventoryDisplay also shows: `{totalItems} items`
|
||||||
|
- Both calculate the same `totalItems` value
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Current Component Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
Game.tsx / Main Game Layout
|
||||||
|
│
|
||||||
|
└── Tabs Component (renders active tab)
|
||||||
|
│
|
||||||
|
└── LootTab ({ store })
|
||||||
|
│
|
||||||
|
├── Card (bg-gray-900/80 border-gray-700)
|
||||||
|
│ ├── CardHeader
|
||||||
|
│ │ └── CardTitle: "💎 Loot Inventory" + Badge
|
||||||
|
│ └── CardContent
|
||||||
|
│ │
|
||||||
|
│ └── LootInventoryDisplay ({ inventory, elements, equipmentInstances, ... })
|
||||||
|
│ │
|
||||||
|
│ ├── GameCard (variant="default" className="w-full")
|
||||||
|
│ │ ├── Header: "Inventory" + Badge + Search/Filter
|
||||||
|
│ │ ├── Separator
|
||||||
|
│ │ ├── ScrollArea
|
||||||
|
│ │ │ ├── Materials Section
|
||||||
|
│ │ │ ├── Essence Section
|
||||||
|
│ │ │ ├── Blueprints Section
|
||||||
|
│ │ │ └── Equipment Section
|
||||||
|
│ │ └── (content)
|
||||||
|
│ │
|
||||||
|
│ └── AlertDialog (Delete Confirmation)
|
||||||
|
│
|
||||||
|
└── (end Card)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Comparison with Other Tabs
|
||||||
|
|
||||||
|
Looking at other tabs in `/src/components/game/tabs/`:
|
||||||
|
|
||||||
|
- **EquipmentTab.tsx**: Uses `GameCard` directly without an outer `Card` wrapper
|
||||||
|
- **CraftingTab.tsx**: Uses multiple `GameCard` components for different sections, no outer `Card`
|
||||||
|
- **GolemancyTab.tsx**: Uses `GameCard` directly
|
||||||
|
- **LabTab.tsx**: Uses `GameCard` directly
|
||||||
|
|
||||||
|
**Pattern**: Most tabs render their content directly using `GameCard` components without an additional `Card` wrapper from the tab itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Summary of Issues to Fix
|
||||||
|
|
||||||
|
1. **Redundant Card Wrapper in LootTab**: The `Card` + `CardHeader` + `CardContent` wrapper in LootTab.tsx is unnecessary since LootInventoryDisplay already provides its own `GameCard` wrapper.
|
||||||
|
|
||||||
|
2. **Duplicate Header**: Both LootTab and LootInventoryDisplay show similar headers with:
|
||||||
|
- Title text ("Loot Inventory" vs "Inventory")
|
||||||
|
- Item count badge
|
||||||
|
- Icon (emoji 💎 vs Gem icon)
|
||||||
|
|
||||||
|
3. **Double Wrapping**: Content is wrapped in two card-like components:
|
||||||
|
- Outer: `Card` from `@/components/ui/card`
|
||||||
|
- Inner: `GameCard` from `@/components/ui/game-card`
|
||||||
|
|
||||||
|
4. **Solution Direction**: Remove the outer `Card` wrapper from LootTab.tsx and let LootInventoryDisplay handle the card styling, OR remove the inner `GameCard` from LootInventoryDisplay and keep only the outer wrapper.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Files That Would Need Modification
|
||||||
|
|
||||||
|
1. **`/src/components/game/tabs/LootTab.tsx`** - Remove outer Card wrapper, pass props directly to LootInventoryDisplay
|
||||||
|
2. **`/src/components/game/LootInventory.tsx`** - Potentially remove GameCard wrapper if keeping LootTab's Card, or keep as-is if removing LootTab's Card
|
||||||
|
|
||||||
|
**Recommended Approach**: Remove the outer `Card` wrapper from `LootTab.tsx` and let `LootInventoryDisplay` handle the full display (since it already has a complete GameCard wrapper with header, controls, and content).
|
||||||
Generated
+17
@@ -84,6 +84,7 @@
|
|||||||
"bun-types": "^1.3.4",
|
"bun-types": "^1.3.4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16.1.1",
|
"eslint-config-next": "^16.1.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^29.0.1",
|
"jsdom": "^29.0.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
@@ -9619,6 +9620,22 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "9.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||||
|
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"husky": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/icu-minify": {
|
"node_modules/icu-minify": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.9.1.tgz",
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,8 @@
|
|||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:reset": "prisma migrate reset"
|
"db:reset": "prisma migrate reset",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
"bun-types": "^1.3.4",
|
"bun-types": "^1.3.4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16.1.1",
|
"eslint-config-next": "^16.1.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^29.0.1",
|
"jsdom": "^29.0.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
|
|||||||
+42
-13
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
import { useEffect, useState, lazy, Suspense } from 'react';
|
||||||
import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
|
import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
|
||||||
|
import { ActivityLogEntry } from '@/lib/game/types';
|
||||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||||
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||||
@@ -271,9 +272,10 @@ export default function ManaLoopGame() {
|
|||||||
disabled={store.isDescending}
|
disabled={store.isDescending}
|
||||||
>
|
>
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
{store.isDescending ? 'Descending...' : 'Begin Descent'}
|
{store.isDescending ? 'Descending…' :
|
||||||
|
store.currentAction === 'climb' ? 'Climbing' :
|
||||||
|
'Begin Descent'}
|
||||||
</Button>
|
</Button>
|
||||||
{store.currentFloor === 1 ? (
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-green-600 hover:bg-green-700"
|
||||||
@@ -281,11 +283,6 @@ export default function ManaLoopGame() {
|
|||||||
>
|
>
|
||||||
Exit Spire
|
Exit Spire
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-400 flex items-center">
|
|
||||||
Reach floor 1 to exit
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -299,16 +296,48 @@ export default function ManaLoopGame() {
|
|||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ScrollArea className="h-32">
|
<ScrollArea className="h-48">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{store.log.slice(0, 20).map((entry, i) => (
|
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
|
||||||
|
// Style based on event type
|
||||||
|
const getEventStyle = (eventType: string) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'enemy_defeated':
|
||||||
|
case 'floor_cleared':
|
||||||
|
return 'text-green-400';
|
||||||
|
case 'damage_dealt':
|
||||||
|
return 'text-red-400';
|
||||||
|
case 'dodge':
|
||||||
|
return 'text-yellow-400';
|
||||||
|
case 'armor_proc':
|
||||||
|
return 'text-blue-400';
|
||||||
|
case 'special_effect':
|
||||||
|
return 'text-purple-400';
|
||||||
|
case 'floor_transition':
|
||||||
|
return 'text-cyan-400';
|
||||||
|
case 'spell_cast':
|
||||||
|
return 'text-amber-400';
|
||||||
|
case 'golem_attack':
|
||||||
|
return 'text-orange-400';
|
||||||
|
case 'puzzle_solved':
|
||||||
|
return 'text-pink-400';
|
||||||
|
default:
|
||||||
|
return 'text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={entry.id}
|
||||||
className={`text-sm ${i === 0 ? 'text-gray-200' : 'text-gray-500'} italic`}
|
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
|
||||||
>
|
>
|
||||||
{entry}
|
{entry.message}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
{(store.activityLog || []).length === 0 && (
|
||||||
|
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,486 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage, getEnemyName } from '@/lib/game/store';
|
|
||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants';
|
|
||||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
|
||||||
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { X, BookOpen, Skull, Shield, Wind } from 'lucide-react';
|
|
||||||
|
|
||||||
export function SpireTab() {
|
|
||||||
const store = useGameStore();
|
|
||||||
const { effectiveRegen, meditationMultiplier, incursionStrength } = useManaStats();
|
|
||||||
const {
|
|
||||||
floorElem, floorElemDef, isGuardianFloor, currentGuardian,
|
|
||||||
activeSpellDef, dps, damageBreakdown
|
|
||||||
} = useCombatStats();
|
|
||||||
const { effectiveStudySpeedMult } = useStudyStats();
|
|
||||||
|
|
||||||
// Get room type info
|
|
||||||
const currentRoom = store.currentRoom;
|
|
||||||
const roomType = currentRoom?.roomType || 'combat';
|
|
||||||
const roomConfig = ROOM_TYPE_LABELS[roomType] || ROOM_TYPE_LABELS.combat;
|
|
||||||
|
|
||||||
// Check if spell can be cast
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get enemy display info
|
|
||||||
const getEnemyDisplayInfo = () => {
|
|
||||||
if (!currentRoom || !currentRoom.enemies || currentRoom.enemies.length === 0) {
|
|
||||||
return { primaryEnemy: null, swarmEnemies: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const enemies = currentRoom.enemies;
|
|
||||||
const primaryEnemy = enemies[0];
|
|
||||||
|
|
||||||
// For swarm rooms, return all enemies
|
|
||||||
if (roomType === 'swarm') {
|
|
||||||
return { primaryEnemy: null, swarmEnemies: enemies };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { primaryEnemy, swarmEnemies: [] };
|
|
||||||
};
|
|
||||||
|
|
||||||
const { primaryEnemy, swarmEnemies } = getEnemyDisplayInfo();
|
|
||||||
|
|
||||||
// Render study progress
|
|
||||||
const renderStudyProgress = () => {
|
|
||||||
if (!store.currentStudyTarget) return null;
|
|
||||||
|
|
||||||
const target = store.currentStudyTarget;
|
|
||||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
|
||||||
const isSkill = target.type === 'skill';
|
|
||||||
const def = isSkill ? SPELLS_DEF[target.id] : SPELLS_DEF[target.id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4 text-purple-400" />
|
|
||||||
<span className="text-sm font-semibold text-purple-300">
|
|
||||||
{def?.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={() => store.cancelStudy()}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}</span>
|
|
||||||
<span>{effectiveStudySpeedMult.toFixed(1)}x speed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{/* Current Floor Card */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
|
|
||||||
<span>Current Floor</span>
|
|
||||||
<Badge
|
|
||||||
className="ml-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${roomConfig.color}20`,
|
|
||||||
color: roomConfig.color,
|
|
||||||
borderColor: `${roomConfig.color}60`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{roomConfig.icon} {roomConfig.label}
|
|
||||||
</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span className="text-4xl font-bold game-title" style={{ color: floorElemDef?.color }}>
|
|
||||||
{store.currentFloor}
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-400 text-sm">/ 100</span>
|
|
||||||
<span className="ml-auto text-sm" style={{ color: floorElemDef?.color }}>
|
|
||||||
{floorElemDef?.sym} {floorElemDef?.name}
|
|
||||||
</span>
|
|
||||||
{isGuardianFloor && (
|
|
||||||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isGuardianFloor && currentGuardian && (
|
|
||||||
<div className="text-sm font-semibold game-panel-title" style={{ color: floorElemDef?.color }}>
|
|
||||||
⚔️ {currentGuardian.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Single Enemy Display (Combat/Speed/Guardian) */}
|
|
||||||
{!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && (
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skull className="w-4 h-4 text-red-400" />
|
|
||||||
<span className="text-sm font-semibold text-gray-200">
|
|
||||||
{primaryEnemy.name || 'Unknown Enemy'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enemy HP Bar */}
|
|
||||||
<div className="space-y-1 mb-2">
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (primaryEnemy.hp / primaryEnemy.maxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
|
||||||
<span>{fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enemy Properties */}
|
|
||||||
<div className="flex flex-wrap gap-2 text-xs">
|
|
||||||
{primaryEnemy.armor > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Shield className="w-3 h-3 mr-1" />
|
|
||||||
{(primaryEnemy.armor * 100).toFixed(0)}% Armor
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{primaryEnemy.dodgeChance > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Wind className="w-3 h-3 mr-1" />
|
|
||||||
{(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Chance to dodge attacks and reduce progress</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Swarm Enemies Display */}
|
|
||||||
{roomType === 'swarm' && swarmEnemies.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-gray-400 font-semibold">
|
|
||||||
Swarm Enemies ({swarmEnemies.length})
|
|
||||||
</div>
|
|
||||||
{swarmEnemies.map((enemy, index) => (
|
|
||||||
<div key={enemy.id || `swarm-${index}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skull className="w-3 h-3 text-red-400" />
|
|
||||||
<span className="text-xs font-semibold text-gray-300">
|
|
||||||
{enemy.name || `Enemy ${index + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
{ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (enemy.hp / enemy.maxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${ELEMENTS[enemy.element]?.color}99, ${ELEMENTS[enemy.element]?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Puzzle Room Display */}
|
|
||||||
{roomType === 'puzzle' && (
|
|
||||||
<div className="p-3 bg-purple-900/20 rounded border border-purple-700">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className="text-lg">🧩</span>
|
|
||||||
<span className="text-sm font-semibold text-purple-300">
|
|
||||||
{currentRoom.puzzleId ? currentRoom.puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>Progress</span>
|
|
||||||
<span>{((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
value={Math.min(100, (currentRoom.puzzleProgress || 0) * 100)}
|
|
||||||
className="h-2 bg-gray-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Floor HP Bar (for non-swarm, non-puzzle) */}
|
|
||||||
{roomType !== 'swarm' && roomType !== 'puzzle' && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (store.floorHP / store.floorMaxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
|
||||||
boxShadow: `0 0 10px ${floorElemDef?.glow}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
|
||||||
<span>{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP</span>
|
|
||||||
<span>DPS: {store.currentAction === 'climb' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong> •
|
|
||||||
Pacts: <strong className="text-amber-400">{store.signedPacts.length}</strong>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Active Spell Card */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Spell</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
{activeSpellDef ? (
|
|
||||||
<>
|
|
||||||
<div className="text-lg font-semibold game-panel-title" style={{ color: activeSpellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[activeSpellDef.elem]?.color }}>
|
|
||||||
{activeSpellDef.name}
|
|
||||||
{activeSpellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200">Basic</Badge>}
|
|
||||||
{activeSpellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100">Legendary</Badge>}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-400 game-mono">
|
|
||||||
⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg •
|
|
||||||
<span style={{ color: getSpellCostColor(activeSpellDef.cost) }}>
|
|
||||||
{' '}{formatSpellCost(activeSpellDef.cost)}
|
|
||||||
</span>
|
|
||||||
{' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cast progress bar when climbing */}
|
|
||||||
{store.currentAction === 'climb' && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>Cast Progress</span>
|
|
||||||
<span>{((store.castProgress || 0) * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, (store.castProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSpellDef.desc && (
|
|
||||||
<div className="text-xs text-gray-500 italic">{activeSpellDef.desc}</div>
|
|
||||||
)}
|
|
||||||
{activeSpellDef.effects && activeSpellDef.effects.length > 0 && (
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{activeSpellDef.effects.map((eff, i) => (
|
|
||||||
<Badge key={i} variant="outline" className="text-xs">
|
|
||||||
{eff.type === 'burn' && `🔥 Burn ${eff.value}/hr`}
|
|
||||||
{eff.type === 'stun' && `⚡ Stun ${eff.value}s`}
|
|
||||||
{eff.type === 'pierce' && `🗡️ Pierce ${Math.round(eff.value * 100)}%`}
|
|
||||||
{eff.type === 'multicast' && `✨ ${Math.round(eff.value * 100)}% Multicast`}
|
|
||||||
{eff.type === 'buff' && `💪 Buff`}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500">No spell selected</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Can cast indicator */}
|
|
||||||
{activeSpellDef && (
|
|
||||||
<div className={`text-xs ${canCastSpell(store.activeSpell) ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{incursionStrength > 0 && (
|
|
||||||
<div className="p-2 bg-red-900/20 border border-red-800/50 rounded">
|
|
||||||
<div className="text-xs text-red-400 game-panel-title mb-1">LABYRINTH INCURSION</div>
|
|
||||||
<div className="text-sm text-gray-300">
|
|
||||||
-{Math.round(incursionStrength * 100)}% mana regen
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Current Study (if any) */}
|
|
||||||
{store.currentStudyTarget && (
|
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
|
|
||||||
<CardContent className="pt-4 space-y-3">
|
|
||||||
{renderStudyProgress()}
|
|
||||||
|
|
||||||
{/* Parallel Study Progress */}
|
|
||||||
{store.parallelStudyTarget && (
|
|
||||||
<div className="p-3 rounded border border-cyan-600/50 bg-cyan-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4 text-cyan-400" />
|
|
||||||
<span className="text-sm font-semibold text-cyan-300">
|
|
||||||
Parallel: {store.parallelStudyTarget.type === 'skill' ? store.parallelStudyTarget.id : store.parallelStudyTarget.id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={() => store.cancelParallelStudy()}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)}</span>
|
|
||||||
<span>50% speed (Parallel Study)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pact Signing Progress */}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Spells Available */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Known Spells</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
|
||||||
{Object.entries(store.spells)
|
|
||||||
.filter(([, state]) => state.learned)
|
|
||||||
.map(([id, state]) => {
|
|
||||||
const def = SPELLS_DEF[id];
|
|
||||||
if (!def) return null;
|
|
||||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
|
||||||
const isActive = store.activeSpell === id;
|
|
||||||
const canCast = canCastSpell(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={id}
|
|
||||||
variant="outline"
|
|
||||||
className={`h-auto py-2 px-3 flex flex-col items-start ${isActive ? 'border-amber-500 bg-amber-900/20' : canCast ? 'border-gray-600 bg-gray-800/50 hover:bg-gray-700/50' : 'border-gray-700 bg-gray-800/30 opacity-60'}`}
|
|
||||||
onClick={() => store.setSpell(id)}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-semibold" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
|
||||||
{def.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 game-mono">
|
|
||||||
{fmt(calcDamage(store, id))} dmg
|
|
||||||
</div>
|
|
||||||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
|
||||||
{formatSpellCost(def.cost)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Activity Log */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ScrollArea className="h-48">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
|
|
||||||
// Style based on event type
|
|
||||||
const getEventStyle = (eventType: string) => {
|
|
||||||
switch (eventType) {
|
|
||||||
case 'enemy_defeated':
|
|
||||||
case 'floor_cleared':
|
|
||||||
return 'text-green-400';
|
|
||||||
case 'damage_dealt':
|
|
||||||
return 'text-red-400';
|
|
||||||
case 'dodge':
|
|
||||||
return 'text-yellow-400';
|
|
||||||
case 'armor_proc':
|
|
||||||
return 'text-blue-400';
|
|
||||||
case 'special_effect':
|
|
||||||
return 'text-purple-400';
|
|
||||||
case 'floor_transition':
|
|
||||||
return 'text-cyan-400';
|
|
||||||
case 'spell_cast':
|
|
||||||
return 'text-amber-400';
|
|
||||||
case 'golem_attack':
|
|
||||||
return 'text-orange-400';
|
|
||||||
case 'puzzle_solved':
|
|
||||||
return 'text-pink-400';
|
|
||||||
default:
|
|
||||||
return 'text-gray-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={entry.id}
|
|
||||||
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
|
|
||||||
>
|
|
||||||
{entry.message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{(store.activityLog || []).length === 0 && (
|
|
||||||
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpireTab.displayName = "SpireTab";
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
import { SectionHeader } from '@/components/ui/section-header';
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
import { Stepper } from '@/components/ui/stepper';
|
|
||||||
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
||||||
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, type GameStore } from '@/lib/game/store';
|
import { fmt, type GameStore } from '@/lib/game/store';
|
||||||
@@ -22,9 +20,6 @@ export interface CraftingTabProps {
|
|||||||
store: GameStore;
|
store: GameStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crafting phases for the stepper
|
|
||||||
const CRAFTING_PHASES = ['Design', 'Prepare', 'Apply', 'Craft'];
|
|
||||||
|
|
||||||
export function CraftingTab({ store }: CraftingTabProps) {
|
export function CraftingTab({ store }: CraftingTabProps) {
|
||||||
const showToast = useGameToast();
|
const showToast = useGameToast();
|
||||||
const currentAction = store.currentAction;
|
const currentAction = store.currentAction;
|
||||||
@@ -35,18 +30,8 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
const pauseApplication = store.pauseApplication;
|
const pauseApplication = store.pauseApplication;
|
||||||
const resumeApplication = store.resumeApplication;
|
const resumeApplication = store.resumeApplication;
|
||||||
|
|
||||||
const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft');
|
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
|
||||||
|
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
|
||||||
// Map crafting stage to stepper index
|
|
||||||
const getStepperIndex = (stage: string): number => {
|
|
||||||
switch (stage) {
|
|
||||||
case 'design': return 0;
|
|
||||||
case 'prepare': return 1;
|
|
||||||
case 'apply': return 2;
|
|
||||||
case 'craft': return 3;
|
|
||||||
default: return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Safe toFixed helper
|
// Safe toFixed helper
|
||||||
const safeToFixed = (value: number | undefined, decimals: number = 0): string => {
|
const safeToFixed = (value: number | undefined, decimals: number = 0): string => {
|
||||||
@@ -72,21 +57,71 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
<div className="space-y-4 max-w-full overflow-x-hidden">
|
||||||
{/* Visual Stepper - Requirement: show Design, Prepare, Apply phases as visual stepper */}
|
{/* Top Sub-Tabs: Fabricate / Enchant */}
|
||||||
<GameCard variant="default" className="p-4">
|
<GameCard variant="default" className="p-4">
|
||||||
<Stepper
|
<div className="flex justify-center gap-2">
|
||||||
steps={CRAFTING_PHASES}
|
<ActionButton
|
||||||
currentStep={getStepperIndex(craftingStage)}
|
variant={activeTab === 'fabricate' ? 'primary' : 'secondary'}
|
||||||
className="px-4"
|
onClick={() => setActiveTab('fabricate')}
|
||||||
/>
|
className={activeTab === 'fabricate' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Anvil size={14} className="mr-1" />
|
||||||
|
Fabricate
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={activeTab === 'enchant' ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => setActiveTab('enchant')}
|
||||||
|
className={activeTab === 'enchant' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Sparkles size={14} className="mr-1" />
|
||||||
|
Enchant
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
</GameCard>
|
</GameCard>
|
||||||
|
|
||||||
{/* Stage Content - Without unlabeled Tabs, using conditional rendering instead */}
|
{/* Fabricate Content: EquipmentCrafter */}
|
||||||
<div className="mt-4">
|
{activeTab === 'fabricate' && (
|
||||||
{craftingStage === 'craft' && (
|
|
||||||
<EquipmentCrafter store={store} />
|
<EquipmentCrafter store={store} />
|
||||||
)}
|
)}
|
||||||
{craftingStage === 'design' && (
|
|
||||||
|
{/* Enchant Content: Design → Prepare → Apply workflow */}
|
||||||
|
{activeTab === 'enchant' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Enchant Sub-Navigation (no numbered stepper) */}
|
||||||
|
<GameCard variant="default" className="p-4">
|
||||||
|
<div className="flex justify-center gap-2 flex-wrap">
|
||||||
|
<ActionButton
|
||||||
|
variant={enchantStage === 'design' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEnchantStage('design')}
|
||||||
|
className={enchantStage === 'design' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Scroll size={14} className="mr-1" />
|
||||||
|
Design
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={enchantStage === 'prepare' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEnchantStage('prepare')}
|
||||||
|
className={enchantStage === 'prepare' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Hammer size={14} className="mr-1" />
|
||||||
|
Prepare
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
variant={enchantStage === 'apply' ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEnchantStage('apply')}
|
||||||
|
className={enchantStage === 'apply' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||||
|
>
|
||||||
|
<Sparkles size={14} className="mr-1" />
|
||||||
|
Apply
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</GameCard>
|
||||||
|
|
||||||
|
{/* Enchant Stage Content */}
|
||||||
|
{enchantStage === 'design' && (
|
||||||
<EnchantmentDesigner
|
<EnchantmentDesigner
|
||||||
store={store}
|
store={store}
|
||||||
selectedEquipmentType={null}
|
selectedEquipmentType={null}
|
||||||
@@ -99,14 +134,14 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
setSelectedDesign={() => {}}
|
setSelectedDesign={() => {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{craftingStage === 'prepare' && (
|
{enchantStage === 'prepare' && (
|
||||||
<EnchantmentPreparer
|
<EnchantmentPreparer
|
||||||
store={store}
|
store={store}
|
||||||
selectedEquipmentInstance={null}
|
selectedEquipmentInstance={null}
|
||||||
setSelectedEquipmentInstance={() => {}}
|
setSelectedEquipmentInstance={() => {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{craftingStage === 'apply' && (
|
{enchantStage === 'apply' && (
|
||||||
<EnchantmentApplier
|
<EnchantmentApplier
|
||||||
store={store}
|
store={store}
|
||||||
selectedEquipmentInstance={null}
|
selectedEquipmentInstance={null}
|
||||||
@@ -118,50 +153,9 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Stage Navigation Buttons */}
|
{/* Current Activity Indicator: Crafting */}
|
||||||
<GameCard variant="default" className="p-4">
|
|
||||||
<div className="flex justify-center gap-2 flex-wrap">
|
|
||||||
<ActionButton
|
|
||||||
variant={craftingStage === 'craft' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCraftingStage('craft')}
|
|
||||||
className={craftingStage === 'craft' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Anvil size={14} className="mr-1" />
|
|
||||||
Craft
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={craftingStage === 'design' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCraftingStage('design')}
|
|
||||||
className={craftingStage === 'design' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Scroll size={14} className="mr-1" />
|
|
||||||
Design
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={craftingStage === 'prepare' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCraftingStage('prepare')}
|
|
||||||
className={craftingStage === 'prepare' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Hammer size={14} className="mr-1" />
|
|
||||||
Prepare
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={craftingStage === 'apply' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCraftingStage('apply')}
|
|
||||||
className={craftingStage === 'apply' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Sparkles size={14} className="mr-1" />
|
|
||||||
Apply
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Current Activity Indicator */}
|
|
||||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||||
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -183,6 +177,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</GameCard>
|
</GameCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Current Activity Indicator: Designing */}
|
||||||
{currentAction === 'design' && designProgress && (
|
{currentAction === 'design' && designProgress && (
|
||||||
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -204,6 +199,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</GameCard>
|
</GameCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Current Activity Indicator: Preparing */}
|
||||||
{currentAction === 'prepare' && preparationProgress && (
|
{currentAction === 'prepare' && preparationProgress && (
|
||||||
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -228,6 +224,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
</GameCard>
|
</GameCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Current Activity Indicator: Enchanting */}
|
||||||
{currentAction === 'enchant' && applicationProgress && (
|
{currentAction === 'enchant' && applicationProgress && (
|
||||||
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
@@ -238,7 +235,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
|||||||
<ActionButton size="sm" onClick={resumeApplication}>Resume</ActionButton>
|
<ActionButton size="sm" onClick={resumeApplication}>Resume</ActionButton>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ActionButton variant="outline" size="sm" onClick={pauseApplication}>Pause</ActionButton>
|
<ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton>
|
||||||
<ActionButton variant="ghost" size="sm" onClick={() => {
|
<ActionButton variant="ghost" size="sm" onClick={() => {
|
||||||
store.cancelApplication();
|
store.cancelApplication();
|
||||||
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import type { GameStore } from '@/lib/game/store';
|
import type { GameStore } from '@/lib/game/store';
|
||||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||||
|
|
||||||
@@ -14,24 +12,7 @@ export function LootTab({ store }: LootTabProps) {
|
|||||||
const elements = store.elements;
|
const elements = store.elements;
|
||||||
const equipmentInstances = store.equipmentInstances;
|
const equipmentInstances = store.equipmentInstances;
|
||||||
|
|
||||||
// Count items for badge
|
|
||||||
const materialCount = Object.values(inventory.materials).reduce((a, b) => a + b, 0);
|
|
||||||
const blueprintCount = inventory.blueprints.length;
|
|
||||||
const equipmentCount = Object.keys(equipmentInstances).length;
|
|
||||||
const totalItems = materialCount + blueprintCount + equipmentCount;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
|
||||||
💎 Loot Inventory
|
|
||||||
<Badge className="ml-auto bg-gray-800 text-gray-300">
|
|
||||||
{totalItems} items
|
|
||||||
</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<LootInventoryDisplay
|
<LootInventoryDisplay
|
||||||
inventory={inventory}
|
inventory={inventory}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
@@ -39,9 +20,6 @@ export function LootTab({ store }: LootTabProps) {
|
|||||||
onDeleteMaterial={store.deleteMaterial}
|
onDeleteMaterial={store.deleteMaterial}
|
||||||
onDeleteEquipment={store.deleteEquipmentInstance}
|
onDeleteEquipment={store.deleteEquipmentInstance}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { ActivityLogEntry } from '@/lib/game/types';
|
|||||||
import type { GameStore } from '@/lib/game/store';
|
import type { GameStore } from '@/lib/game/store';
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants';
|
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants';
|
||||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||||
import { fmt, fmtDec, getFloorElement, canAffordSpellCost, getEnemyName } from '@/lib/game/store';
|
import { fmt, fmtDec, getFloorElement, canAffordSpellCost, getEnemyName, calcDamage } from '@/lib/game/store';
|
||||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||||
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
||||||
import { CraftingProgress, StudyProgress } from '@/components/game';
|
import { CraftingProgress, StudyProgress } from '@/components/game';
|
||||||
@@ -353,11 +353,11 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 game-mono mb-1">
|
<div className="text-xs text-gray-400 game-mono mb-1">
|
||||||
⚔️ {fmt(totalDPS)} DPS •
|
⚔️ {fmt(calcDamage(store, spellId))} dmg/cast •
|
||||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||||
{' '}{formatSpellCost(spellDef.cost)}
|
{' '}{formatSpellCost(spellDef.cost)}
|
||||||
</span>
|
</span>
|
||||||
{' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr
|
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cast progress bar when climbing */}
|
{/* Cast progress bar when climbing */}
|
||||||
|
|||||||
@@ -847,8 +847,8 @@ function addActivityLogEntry(
|
|||||||
details?: ActivityLogEntry['details']
|
details?: ActivityLogEntry['details']
|
||||||
): ActivityLogEntry[] {
|
): ActivityLogEntry[] {
|
||||||
const entry = createActivityEntry(eventType, message, details);
|
const entry = createActivityEntry(eventType, message, details);
|
||||||
// Keep last 100 entries, newest first
|
// Keep last 50 entries, newest first (Task 10)
|
||||||
return [entry, ...state.activityLog.slice(0, 99)];
|
return [entry, ...state.activityLog.slice(0, 49)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||||
@@ -2306,13 +2306,10 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Exit Spire Mode - only works when at floor 1
|
// Exit Spire Mode - can exit at any floor (re-entry will resume at same floor)
|
||||||
exitSpireMode: () => {
|
exitSpireMode: () => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
// Only allow exit if at floor 1 (bottom)
|
// Allow exit at any floor for re-entry resume
|
||||||
if (state.currentFloor > 1) {
|
|
||||||
return state; // Can't exit, need to climb down to floor 1 first
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
spireMode: false,
|
spireMode: false,
|
||||||
currentAction: 'meditate',
|
currentAction: 'meditate',
|
||||||
|
|||||||
Reference in New Issue
Block a user