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)
|
||||
```
|
||||
|
||||
*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
|
||||
|
||||
### 0. Task 2 Completion Summary
|
||||
@@ -304,6 +306,27 @@ damage *= effects.myNewStatMultiplier;
|
||||
3. **Add research skill in `constants.ts`**
|
||||
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
|
||||
|
||||
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**: `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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+28
-27
@@ -2,69 +2,70 @@
|
||||
|
||||
## Status Overview
|
||||
- **Start Date**: 2025-05-19
|
||||
- **Current Phase**: PRIORITY 2 (Spire Mode Fixes)
|
||||
- **Overall Progress**: 21% complete (4/19 tasks done)
|
||||
- **Current Phase**: PRIORITY 3 (UI/UX Restructuring)
|
||||
- **Overall Progress**: 42% complete (8/19 tasks done)
|
||||
|
||||
---
|
||||
|
||||
## PRIORITY 0 — Crashes (Fix First, Parallel) ✅ COMPLETED
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| SpellsTab crash diagnosis/fix | Completed | Fixed unprotected ENCHANTMENT_EFFECTS access, added spell.cost guards |
|
||||
| LabTab crash diagnosis/fix | Completed | Added safe access to store.elements with `|| {}` fallbacks |
|
||||
| DebugTab crash diagnosis/fix | Completed | Moved Toaster/GameToaster inside DebugProvider in layout.tsx |
|
||||
| SpellsTab crash diagnosis/fix | Completed | Fixed unprotected ENCHANTMENT_EFFECTS access |
|
||||
| LabTab crash diagnosis/fix | Completed | Added safe access to store.elements |
|
||||
| DebugTab crash diagnosis/fix | Completed | Moved Toaster/GameToaster inside DebugProvider |
|
||||
|
||||
---
|
||||
|
||||
## PRIORITY 1 — Mana Conversion Mechanic Fix ✅ COMPLETED
|
||||
| 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
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| 2a. Floor Rendering & Identity (type, named enemy, special properties) | Pending | |
|
||||
| 2b. Swarm Floors (show multiple enemies, verify generation) | Pending | |
|
||||
| 2c. HP Bar Live Updates | Pending | |
|
||||
| 2d. Casting Progress Overflow Fix | Pending | |
|
||||
| 2e. Climb/Descend Controls (spam fix, re-entry resume, button rename) | Pending | |
|
||||
| 2f. Activity Log Implementation | Pending | |
|
||||
| 2g. Spell Info Display Fix (dmg/cast vs DPS) | Pending | |
|
||||
| 2a. Floor Rendering & Identity | Pending | Context gathering next |
|
||||
| 2b. Swarm Floors | ✅ Completed | Verified by check sub-agent |
|
||||
| 2c. HP Bar Live Updates | ✅ Completed | floorHP synced to enemy HP |
|
||||
| 2d. Casting Progress Overflow | Pending | Context gathering next |
|
||||
| 2e. Climb/Descend Controls | Pending | Context gathering next |
|
||||
| 2f. Activity Log Implementation | Pending | Context gathering next |
|
||||
| 2g. Spell Info Display Fix | Pending | Context gathering next |
|
||||
|
||||
---
|
||||
|
||||
## PRIORITY 3 — UI/UX Restructuring
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| 3a. CraftingTab Restructure (remove 1-4 bar, split Fabricate/Enchant, top sub-tabs) | Pending | |
|
||||
| 3b. LootTab Nesting Fix (remove redundant layers) | Pending | |
|
||||
| 3c. AchievementsTab Nesting Fix (remove duplicate headings) | Pending | |
|
||||
| 3a. CraftingTab Restructure | ✅ Completed | Removed stepper, added Fabricate/Enchant tabs |
|
||||
| 3b. LootTab Nesting Fix | ✅ Completed | Removed redundant LootTab wrapper |
|
||||
| 3c. AchievementsTab Nesting Fix | In Progress | Context gathering → execution |
|
||||
|
||||
---
|
||||
|
||||
## PRIORITY 4 — Enchantment Effects & Research
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| 4a. Mana-Type Capacity Enchantment Effects | Pending | Per unlocked mana type |
|
||||
| 4b. Mana Capacity Research Visibility Gate | Pending | Only show if mana type unlocked |
|
||||
| 4c. Skill Requirement Display Bug Fix (undefined Lv.[object Object]) | Pending | |
|
||||
| 4d. Enchantment Power Effect Implementation + Stub Audit | Pending | Replace placeholder, audit all stubs |
|
||||
| 4a. Mana-Type Capacity Enchantment Effects | Pending | Context gathering next |
|
||||
| 4b. Mana Capacity Research Visibility Gate | Pending | Context gathering next |
|
||||
| 4c. Skill Requirement Display Bug Fix | Pending | Context gathering next |
|
||||
| 4d. Enchantment Power Effect Implementation | Pending | Partially done |
|
||||
|
||||
---
|
||||
|
||||
## PRIORITY 5 — Insight Upgrade Analysis
|
||||
| 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
|
||||
- ✅ PRIORITY 0 crashes fixed via parallel sub-agents, verified and applied all fixes
|
||||
- ✅ PRIORITY 1 mana conversion fix applied: removed double-counting of conversion drain in store.ts tick logic
|
||||
- Next steps: Dispatch parallel sub-agents for PRIORITY 2 Spire Mode fixes (2a-2g)
|
||||
- Advisor tool will be used for ambiguous design decisions
|
||||
- Sub-agent instructions passed via inline prompts with full context
|
||||
## Workflow Log
|
||||
- ✅ PRIORITY 0 crashes fixed via parallel sub-agents
|
||||
- ✅ PRIORITY 1 mana conversion fix applied
|
||||
- ✅ PRIORITY 2b, 2c verified completed
|
||||
- ✅ Task 12 (CraftingTab) completed
|
||||
- ✅ 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",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.0.1",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
@@ -9619,6 +9620,22 @@
|
||||
"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": {
|
||||
"version": "4.9.1",
|
||||
"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:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset"
|
||||
"db:reset": "prisma migrate reset",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -91,6 +92,7 @@
|
||||
"bun-types": "^1.3.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.0.1",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
|
||||
+52
-23
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
||||
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 { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
@@ -271,21 +272,17 @@ export default function ManaLoopGame() {
|
||||
disabled={store.isDescending}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4 mr-2" />
|
||||
{store.isDescending ? 'Descending...' : 'Begin Descent'}
|
||||
{store.isDescending ? 'Descending…' :
|
||||
store.currentAction === 'climb' ? 'Climbing' :
|
||||
'Begin Descent'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => store.exitSpireMode()}
|
||||
>
|
||||
Exit Spire
|
||||
</Button>
|
||||
{store.currentFloor === 1 ? (
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => store.exitSpireMode()}
|
||||
>
|
||||
Exit Spire
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 flex items-center">
|
||||
Reach floor 1 to exit
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -299,16 +296,48 @@ export default function ManaLoopGame() {
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-32">
|
||||
<ScrollArea className="h-48">
|
||||
<div className="space-y-1">
|
||||
{store.log.slice(0, 20).map((entry, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`text-sm ${i === 0 ? 'text-gray-200' : 'text-gray-500'} italic`}
|
||||
>
|
||||
{entry}
|
||||
</div>
|
||||
))}
|
||||
{(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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
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';
|
||||
@@ -22,9 +20,6 @@ export interface CraftingTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
// Crafting phases for the stepper
|
||||
const CRAFTING_PHASES = ['Design', 'Prepare', 'Apply', 'Craft'];
|
||||
|
||||
export function CraftingTab({ store }: CraftingTabProps) {
|
||||
const showToast = useGameToast();
|
||||
const currentAction = store.currentAction;
|
||||
@@ -35,18 +30,8 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
|
||||
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
|
||||
|
||||
// Safe toFixed helper
|
||||
const safeToFixed = (value: number | undefined, decimals: number = 0): string => {
|
||||
@@ -72,96 +57,105 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Stepper
|
||||
steps={CRAFTING_PHASES}
|
||||
currentStep={getStepperIndex(craftingStage)}
|
||||
className="px-4"
|
||||
/>
|
||||
</GameCard>
|
||||
|
||||
{/* 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 */}
|
||||
<GameCard variant="default" className="p-4">
|
||||
<div className="flex justify-center gap-2 flex-wrap">
|
||||
<div className="flex justify-center gap-2">
|
||||
<ActionButton
|
||||
variant={craftingStage === 'craft' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setCraftingStage('craft')}
|
||||
className={craftingStage === 'craft' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
variant={activeTab === 'fabricate' ? 'primary' : 'secondary'}
|
||||
onClick={() => setActiveTab('fabricate')}
|
||||
className={activeTab === 'fabricate' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Anvil size={14} className="mr-1" />
|
||||
Craft
|
||||
Fabricate
|
||||
</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)]' : ''}
|
||||
variant={activeTab === 'enchant' ? 'primary' : 'secondary'}
|
||||
onClick={() => setActiveTab('enchant')}
|
||||
className={activeTab === 'enchant' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
Apply
|
||||
Enchant
|
||||
</ActionButton>
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Current Activity Indicator */}
|
||||
{/* Fabricate Content: EquipmentCrafter */}
|
||||
{activeTab === 'fabricate' && (
|
||||
<EquipmentCrafter store={store} />
|
||||
)}
|
||||
|
||||
{/* 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
|
||||
store={store}
|
||||
selectedEquipmentType={null}
|
||||
setSelectedEquipmentType={() => {}}
|
||||
selectedEffects={[]}
|
||||
setSelectedEffects={() => {}}
|
||||
designName={''}
|
||||
setDesignName={() => {}}
|
||||
selectedDesign={null}
|
||||
setSelectedDesign={() => {}}
|
||||
/>
|
||||
)}
|
||||
{enchantStage === 'prepare' && (
|
||||
<EnchantmentPreparer
|
||||
store={store}
|
||||
selectedEquipmentInstance={null}
|
||||
setSelectedEquipmentInstance={() => {}}
|
||||
/>
|
||||
)}
|
||||
{enchantStage === 'apply' && (
|
||||
<EnchantmentApplier
|
||||
store={store}
|
||||
selectedEquipmentInstance={null}
|
||||
setSelectedEquipmentInstance={() => {}}
|
||||
selectedDesign={null}
|
||||
setSelectedDesign={() => {}}
|
||||
onEnchantmentApplied={handleEnchantmentApplied}
|
||||
onCapacityExceeded={handleCapacityExceeded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Crafting */}
|
||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
||||
<SectionHeader
|
||||
@@ -183,6 +177,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Designing */}
|
||||
{currentAction === 'design' && designProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
||||
<SectionHeader
|
||||
@@ -204,6 +199,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Preparing */}
|
||||
{currentAction === 'prepare' && preparationProgress && (
|
||||
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
||||
<SectionHeader
|
||||
@@ -228,6 +224,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Enchanting */}
|
||||
{currentAction === 'enchant' && applicationProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
||||
<SectionHeader
|
||||
@@ -238,7 +235,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
<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={() => {
|
||||
store.cancelApplication();
|
||||
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'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';
|
||||
|
||||
@@ -14,34 +12,14 @@ export function LootTab({ store }: LootTabProps) {
|
||||
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>
|
||||
<LootInventoryDisplay
|
||||
inventory={inventory}
|
||||
elements={elements}
|
||||
equipmentInstances={equipmentInstances}
|
||||
onDeleteMaterial={store.deleteMaterial}
|
||||
onDeleteEquipment={store.deleteEquipmentInstance}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ActivityLogEntry } from '@/lib/game/types';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
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 { 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 { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
||||
import { CraftingProgress, StudyProgress } from '@/components/game';
|
||||
@@ -353,11 +353,11 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
</span>
|
||||
</div>
|
||||
<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) }}>
|
||||
{' '}{formatSpellCost(spellDef.cost)}
|
||||
</span>
|
||||
{' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr
|
||||
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||
</div>
|
||||
|
||||
{/* Cast progress bar when climbing */}
|
||||
|
||||
@@ -847,8 +847,8 @@ function addActivityLogEntry(
|
||||
details?: ActivityLogEntry['details']
|
||||
): ActivityLogEntry[] {
|
||||
const entry = createActivityEntry(eventType, message, details);
|
||||
// Keep last 100 entries, newest first
|
||||
return [entry, ...state.activityLog.slice(0, 99)];
|
||||
// Keep last 50 entries, newest first (Task 10)
|
||||
return [entry, ...state.activityLog.slice(0, 49)];
|
||||
}
|
||||
|
||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||
@@ -2306,13 +2306,10 @@ export const useGameStore = create<GameStore>()(
|
||||
}, 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: () => {
|
||||
set((state) => {
|
||||
// Only allow exit if at floor 1 (bottom)
|
||||
if (state.currentFloor > 1) {
|
||||
return state; // Can't exit, need to climb down to floor 1 first
|
||||
}
|
||||
// Allow exit at any floor for re-entry resume
|
||||
return {
|
||||
spireMode: false,
|
||||
currentAction: 'meditate',
|
||||
|
||||
Reference in New Issue
Block a user