fix: resolve 22 remaining issues - type exports, dead code, state mutations, orphaned components
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-18T18:09:58.620Z
|
Generated: 2026-05-18T18:30:51.103Z
|
||||||
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 123 files (1.3s) (29 warnings)
|
1. Processed 124 files (1.3s) (29 warnings)
|
||||||
2. 1) data/equipment/index.ts > data/equipment/utils.ts
|
2. 1) data/equipment/index.ts > data/equipment/utils.ts
|
||||||
3. 2) data/golems/index.ts > data/golems/utils.ts
|
3. 2) data/golems/index.ts > data/golems/utils.ts
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-18T18:09:57.081Z",
|
"generated": "2026-05-18T18:30:49.611Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -260,6 +260,7 @@
|
|||||||
"data/enchantments/spell-effects/index.ts": [
|
"data/enchantments/spell-effects/index.ts": [
|
||||||
"data/enchantment-types.ts",
|
"data/enchantment-types.ts",
|
||||||
"data/enchantments/spell-effects/basic-spells.ts",
|
"data/enchantments/spell-effects/basic-spells.ts",
|
||||||
|
"data/enchantments/spell-effects/legendary-spells.ts",
|
||||||
"data/enchantments/spell-effects/lightning-spells.ts",
|
"data/enchantments/spell-effects/lightning-spells.ts",
|
||||||
"data/enchantments/spell-effects/metal-spells.ts",
|
"data/enchantments/spell-effects/metal-spells.ts",
|
||||||
"data/enchantments/spell-effects/sand-spells.ts",
|
"data/enchantments/spell-effects/sand-spells.ts",
|
||||||
@@ -267,6 +268,9 @@
|
|||||||
"data/enchantments/spell-effects/tier3-spells.ts",
|
"data/enchantments/spell-effects/tier3-spells.ts",
|
||||||
"data/enchantments/spell-effects/types.ts"
|
"data/enchantments/spell-effects/types.ts"
|
||||||
],
|
],
|
||||||
|
"data/enchantments/spell-effects/legendary-spells.ts": [
|
||||||
|
"data/enchantments/spell-effects/types.ts"
|
||||||
|
],
|
||||||
"data/enchantments/spell-effects/lightning-spells.ts": [
|
"data/enchantments/spell-effects/lightning-spells.ts": [
|
||||||
"data/enchantments/spell-effects/types.ts"
|
"data/enchantments/spell-effects/types.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -67,10 +67,8 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── BlueprintsSection.tsx
|
│ │ │ │ ├── BlueprintsSection.tsx
|
||||||
│ │ │ │ ├── EquipmentItem.tsx
|
│ │ │ │ ├── EquipmentItem.tsx
|
||||||
│ │ │ │ ├── EssenceItem.tsx
|
│ │ │ │ ├── EssenceItem.tsx
|
||||||
│ │ │ │ ├── LootInventoryDisplay.tsx
|
|
||||||
│ │ │ │ ├── MaterialItem.tsx
|
│ │ │ │ ├── MaterialItem.tsx
|
||||||
│ │ │ │ ├── icons.ts
|
│ │ │ │ ├── icons.ts
|
||||||
│ │ │ │ ├── index.tsx
|
|
||||||
│ │ │ │ └── types.ts
|
│ │ │ │ └── types.ts
|
||||||
│ │ │ ├── StatsTab/
|
│ │ │ ├── StatsTab/
|
||||||
│ │ │ │ ├── CombatStatsSection.tsx
|
│ │ │ │ ├── CombatStatsSection.tsx
|
||||||
@@ -105,18 +103,13 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── ActivityLog.tsx
|
│ │ │ │ ├── ActivityLog.tsx
|
||||||
│ │ │ │ ├── DisciplinesTab.tsx
|
│ │ │ │ ├── DisciplinesTab.tsx
|
||||||
│ │ │ │ └── index.ts
|
│ │ │ │ └── index.ts
|
||||||
│ │ │ ├── AchievementsDisplay.tsx
|
|
||||||
│ │ │ ├── ActionButtons.tsx
|
│ │ │ ├── ActionButtons.tsx
|
||||||
│ │ │ ├── ActivityLogPanel.tsx
|
│ │ │ ├── ActivityLogPanel.tsx
|
||||||
│ │ │ ├── AttunementStatus.tsx
|
│ │ │ ├── AttunementStatus.tsx
|
||||||
│ │ │ ├── CalendarDisplay.tsx
|
|
||||||
│ │ │ ├── ConfirmDialog.tsx
|
|
||||||
│ │ │ ├── CraftingProgress.tsx
|
|
||||||
│ │ │ ├── GameToast.tsx
|
│ │ │ ├── GameToast.tsx
|
||||||
│ │ │ ├── ManaDisplay.tsx
|
│ │ │ ├── ManaDisplay.tsx
|
||||||
│ │ │ ├── SpellsTab.tsx
|
│ │ │ ├── SpellsTab.tsx
|
||||||
│ │ │ ├── StatsTab.tsx
|
│ │ │ ├── StatsTab.tsx
|
||||||
│ │ │ ├── StudyProgress.tsx
|
|
||||||
│ │ │ ├── TimeDisplay.tsx
|
│ │ │ ├── TimeDisplay.tsx
|
||||||
│ │ │ ├── UpgradeDialog.tsx
|
│ │ │ ├── UpgradeDialog.tsx
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
@@ -191,14 +184,10 @@ Mana-Loop/
|
|||||||
│ │ │ └── preparation-actions.ts
|
│ │ │ └── preparation-actions.ts
|
||||||
│ │ ├── data/
|
│ │ ├── data/
|
||||||
│ │ │ ├── disciplines/
|
│ │ │ ├── disciplines/
|
||||||
│ │ │ │ ├── base-disciplines.ts
|
|
||||||
│ │ │ │ ├── base.ts
|
│ │ │ │ ├── base.ts
|
||||||
│ │ │ │ ├── enchanter-disciplines.ts
|
|
||||||
│ │ │ │ ├── enchanter.ts
|
│ │ │ │ ├── enchanter.ts
|
||||||
│ │ │ │ ├── fabricator-disciplines.ts
|
|
||||||
│ │ │ │ ├── fabricator.ts
|
│ │ │ │ ├── fabricator.ts
|
||||||
│ │ │ │ ├── index.ts
|
│ │ │ │ ├── index.ts
|
||||||
│ │ │ │ ├── invoker-disciplines.ts
|
|
||||||
│ │ │ │ └── invoker.ts
|
│ │ │ │ └── invoker.ts
|
||||||
│ │ │ ├── enchantments/
|
│ │ │ ├── enchantments/
|
||||||
│ │ │ │ ├── spell-effects/
|
│ │ │ │ ├── spell-effects/
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { ManaBar } from '@/components/ui/mana-bar';
|
|
||||||
import { Trophy, Lock, CheckCircle, ChevronDown, ChevronUp } from 'lucide-react';
|
|
||||||
import type { AchievementState } from '@/lib/game/types';
|
|
||||||
import { ACHIEVEMENTS, getAchievementsByCategory, isAchievementRevealed } from '@/lib/game/data/achievements';
|
|
||||||
import { GameState } from '@/lib/game/types';
|
|
||||||
|
|
||||||
// Map achievement categories to CSS variables for colors
|
|
||||||
const CATEGORY_COLOR_MAP: Record<string, string> = {
|
|
||||||
combat: 'var(--color-danger)',
|
|
||||||
progression: 'var(--rarity-legendary)',
|
|
||||||
crafting: 'var(--mana-dark)',
|
|
||||||
magic: 'var(--mana-water)',
|
|
||||||
special: 'var(--mana-stellar)',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AchievementsProps {
|
|
||||||
achievements: AchievementState;
|
|
||||||
gameState: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'totalSpellsCast' | 'totalDamageDealt' | 'totalCraftsCompleted'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AchievementsDisplay({ achievements, gameState }: AchievementsProps) {
|
|
||||||
const [expandedCategory, setExpandedCategory] = useState<string | null>('combat');
|
|
||||||
|
|
||||||
const categories = getAchievementsByCategory();
|
|
||||||
const unlockedCount = achievements.unlocked.length;
|
|
||||||
const totalCount = Object.keys(ACHIEVEMENTS).length;
|
|
||||||
|
|
||||||
// Calculate progress for each achievement
|
|
||||||
const getProgress = (achievementId: string): number => {
|
|
||||||
const achievement = ACHIEVEMENTS[achievementId];
|
|
||||||
if (!achievement) return 0;
|
|
||||||
if (achievements.unlocked.includes(achievementId)) return achievement.requirement.value;
|
|
||||||
|
|
||||||
const { type, subType } = achievement.requirement;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'floor':
|
|
||||||
if (subType === 'noPacts') {
|
|
||||||
return gameState.maxFloorReached >= achievement.requirement.value && gameState.signedPacts.length === 0
|
|
||||||
? achievement.requirement.value
|
|
||||||
: gameState.maxFloorReached;
|
|
||||||
}
|
|
||||||
return gameState.maxFloorReached;
|
|
||||||
case 'spells':
|
|
||||||
return gameState.totalSpellsCast || 0;
|
|
||||||
case 'damage':
|
|
||||||
return gameState.totalDamageDealt || 0;
|
|
||||||
case 'mana':
|
|
||||||
return gameState.totalManaGathered || 0;
|
|
||||||
case 'pact':
|
|
||||||
return gameState.signedPacts.length;
|
|
||||||
case 'craft':
|
|
||||||
return gameState.totalCraftsCompleted || 0;
|
|
||||||
default:
|
|
||||||
return achievements.progress[achievementId] || 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameCard variant="default" className="w-full">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Trophy className="w-4 h-4 text-[var(--mana-light)]" />
|
|
||||||
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
|
||||||
Achievements
|
|
||||||
</h3>
|
|
||||||
<Badge
|
|
||||||
className="ml-auto bg-[var(--bg-sunken)] text-[var(--text-secondary)] border-[var(--border-subtle)]"
|
|
||||||
aria-label={`${unlockedCount} out of ${totalCount} achievements unlocked`}
|
|
||||||
>
|
|
||||||
{unlockedCount} / {totalCount}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="h-64 w-full">
|
|
||||||
<div className="space-y-2 pr-2">
|
|
||||||
{Object.entries(categories).map(([category, categoryAchievements]) => (
|
|
||||||
<div key={category} className="space-y-1">
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="w-full justify-between text-xs hover:bg-[var(--bg-sunken)]"
|
|
||||||
onClick={() => setExpandedCategory(expandedCategory === category ? null : category)}
|
|
||||||
aria-expanded={expandedCategory === category}
|
|
||||||
aria-label={`${category} category - ${categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} of ${categoryAchievements.length} unlocked`}
|
|
||||||
>
|
|
||||||
<span style={{ color: CATEGORY_COLOR_MAP[category] || 'var(--text-primary)' }}>
|
|
||||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
|
||||||
</span>
|
|
||||||
<span className="text-[var(--text-muted)]">
|
|
||||||
{categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} / {categoryAchievements.length}
|
|
||||||
</span>
|
|
||||||
{expandedCategory === category ? (
|
|
||||||
<ChevronUp className="w-4 h-4 text-[var(--text-muted)]" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="w-4 h-4 text-[var(--text-muted)]" />
|
|
||||||
)}
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
{expandedCategory === category && (
|
|
||||||
<div className="pl-2 space-y-2">
|
|
||||||
{categoryAchievements.map((achievement) => {
|
|
||||||
const isUnlocked = achievements.unlocked.includes(achievement.id);
|
|
||||||
const progress = getProgress(achievement.id);
|
|
||||||
const isRevealed = isAchievementRevealed(achievement, progress);
|
|
||||||
const progressPercent = Math.min(100, (progress / achievement.requirement.value) * 100);
|
|
||||||
|
|
||||||
if (!isRevealed && !isUnlocked) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={achievement.id}
|
|
||||||
className="p-2 rounded bg-[var(--bg-sunken)] border border-[var(--border-subtle)]"
|
|
||||||
aria-label="Locked achievement - details hidden"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-[var(--text-muted)]">
|
|
||||||
<Lock className="w-4 h-4" aria-hidden="true" />
|
|
||||||
<span className="text-sm">???</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={achievement.id}
|
|
||||||
className={`p-2 rounded border ${
|
|
||||||
isUnlocked
|
|
||||||
? 'bg-[var(--rarity-legendary-glow)] border-[var(--rarity-legendary)]/50'
|
|
||||||
: 'bg-[var(--bg-sunken)] border-[var(--border-subtle)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between mb-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isUnlocked ? (
|
|
||||||
<CheckCircle className="w-4 h-4 text-[var(--mana-light)]" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<Trophy className="w-4 h-4 text-[var(--text-muted)]" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold ${
|
|
||||||
isUnlocked ? 'text-[var(--mana-light)]' : 'text-[var(--text-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{achievement.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{achievement.reward.title && isUnlocked && (
|
|
||||||
<Badge
|
|
||||||
className="text-xs bg-[var(--mana-dark)]/20 text-[var(--mana-dark)] border-[var(--mana-dark)]/40"
|
|
||||||
aria-label="Title reward"
|
|
||||||
>
|
|
||||||
Title
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2">
|
|
||||||
{achievement.desc}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isUnlocked && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<ManaBar
|
|
||||||
value={progress}
|
|
||||||
max={achievement.requirement.value}
|
|
||||||
manaType="light"
|
|
||||||
className="h-1.5"
|
|
||||||
aria-label={`Progress: ${Math.round(progressPercent)}%`}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
|
||||||
<span>{progress.toLocaleString()} / {achievement.requirement.value.toLocaleString()}</span>
|
|
||||||
<span>{progressPercent.toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isUnlocked && achievement.reward && (
|
|
||||||
<div className="text-xs text-[var(--mana-light)]/70">
|
|
||||||
Reward:
|
|
||||||
{achievement.reward.insight && ` +${achievement.reward.insight} Insight`}
|
|
||||||
{achievement.reward.manaBonus && ` +${achievement.reward.manaBonus} Max Mana`}
|
|
||||||
{achievement.reward.damageBonus && ` +${(achievement.reward.damageBonus * 100).toFixed(0)}% Damage`}
|
|
||||||
{achievement.reward.title && ` "${achievement.reward.title}"`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AchievementsDisplay.displayName = "AchievementsDisplay";
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface CalendarDisplayProps {
|
|
||||||
day: number;
|
|
||||||
hour: number;
|
|
||||||
incursionStrength?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CalendarDisplay({ day }: CalendarDisplayProps) {
|
|
||||||
const days: React.ReactElement[] = [];
|
|
||||||
|
|
||||||
for (let d = 1; d <= MAX_DAY; d++) {
|
|
||||||
let dayClass = 'w-6 h-6 sm:w-7 sm:h-7 rounded text-xs flex items-center justify-center font-mono border transition-all ';
|
|
||||||
|
|
||||||
if (d < day) {
|
|
||||||
dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400';
|
|
||||||
} else if (d === day) {
|
|
||||||
dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30';
|
|
||||||
} else {
|
|
||||||
dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (d >= INCURSION_START_DAY) {
|
|
||||||
dayClass += ' border-red-600/50';
|
|
||||||
}
|
|
||||||
|
|
||||||
days.push(
|
|
||||||
<Tooltip key={d}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className={dayClass}>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Day {d}</p>
|
|
||||||
{d >= INCURSION_START_DAY && <p className="text-red-400">Incursion Active</p>}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-7 sm:grid-cols-7 md:grid-cols-14 gap-1">
|
|
||||||
{days}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDisplay.displayName = "CalendarDisplay";
|
|
||||||
CalendarDisplay.displayName = "CalendarDisplay";
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, type ReactNode } from 'react';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
import { AlertTriangle, AlertCircle, Info, CheckCircle } from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type ConfirmDialogVariant = 'danger' | 'warning' | 'info' | 'success';
|
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
|
||||||
/** Whether the dialog is open */
|
|
||||||
open: boolean;
|
|
||||||
/** Callback when open state changes */
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
/** Dialog title */
|
|
||||||
title: string;
|
|
||||||
/** Dialog description/content */
|
|
||||||
description: ReactNode;
|
|
||||||
/** Cancel button text (default: "Cancel") */
|
|
||||||
cancelText?: string;
|
|
||||||
/** Confirm button text (default: "Confirm") */
|
|
||||||
confirmText?: string;
|
|
||||||
/** Dialog variant/type */
|
|
||||||
variant?: ConfirmDialogVariant;
|
|
||||||
/** Callback when user confirms */
|
|
||||||
onConfirm: () => void | Promise<void>;
|
|
||||||
/** Callback when user cancels */
|
|
||||||
onCancel?: () => void;
|
|
||||||
/** Whether the confirm action is destructive */
|
|
||||||
destructive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VARIANT_ICONS = {
|
|
||||||
danger: AlertTriangle,
|
|
||||||
warning: AlertCircle,
|
|
||||||
info: Info,
|
|
||||||
success: CheckCircle,
|
|
||||||
};
|
|
||||||
|
|
||||||
const VARIANT_TITLE_COLORS = {
|
|
||||||
danger: 'text-[var(--color-danger)]',
|
|
||||||
warning: 'text-[var(--color-warning)]',
|
|
||||||
info: 'text-[var(--color-info)]',
|
|
||||||
success: 'text-[var(--color-success)]',
|
|
||||||
};
|
|
||||||
|
|
||||||
const VARIANT_ACTION_COLORS = {
|
|
||||||
danger: 'bg-[var(--color-danger)] hover:bg-[var(--interactive-danger-hover)] text-white',
|
|
||||||
warning: 'bg-[var(--color-warning)] hover:opacity-90 text-black',
|
|
||||||
info: 'bg-[var(--color-info)] hover:opacity-90 text-white',
|
|
||||||
success: 'bg-[var(--color-success)] hover:opacity-90 text-white',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable confirmation dialog component.
|
|
||||||
* Uses the existing shadcn/ui AlertDialog.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <ConfirmDialog
|
|
||||||
* open={showDialog}
|
|
||||||
* onOpenChange={setShowDialog}
|
|
||||||
* title="Delete Item"
|
|
||||||
* description="Are you sure you want to delete this item? This action cannot be undone."
|
|
||||||
* variant="danger"
|
|
||||||
* onConfirm={handleDelete}
|
|
||||||
* />
|
|
||||||
*/
|
|
||||||
export function ConfirmDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
cancelText = 'Cancel',
|
|
||||||
confirmText = 'Confirm',
|
|
||||||
variant = 'warning',
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
destructive = false,
|
|
||||||
}: ConfirmDialogProps) {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const Icon = VARIANT_ICONS[variant];
|
|
||||||
const titleColor = VARIANT_TITLE_COLORS[variant];
|
|
||||||
const actionClass = destructive ? VARIANT_ACTION_COLORS.danger : VARIANT_ACTION_COLORS[variant];
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await onConfirm();
|
|
||||||
onOpenChange(false);
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : 'Action failed');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
onCancel?.();
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<AlertDialogContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className={cn('flex items-center gap-2', titleColor)}>
|
|
||||||
<Icon className="h-5 w-5" />
|
|
||||||
{title}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-[var(--text-secondary)]">
|
|
||||||
{description}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
{error && (
|
|
||||||
<div className="mt-2 rounded-md bg-red-900/30 border border-red-700/50 px-3 py-2 text-sm text-red-300">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel
|
|
||||||
className="bg-[var(--bg-sunken)] border-[var(--border-default)] text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
{cancelText}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className={cn(actionClass, isLoading && 'opacity-50 cursor-not-allowed')}
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Processing...' : confirmText}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to easily manage a confirmation dialog state.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { dialogProps, showConfirm } = useConfirmDialog();
|
|
||||||
*
|
|
||||||
* showConfirm({
|
|
||||||
* title: "Delete Item",
|
|
||||||
* description: "Are you sure?",
|
|
||||||
* onConfirm: () => deleteItem(),
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useConfirmDialog() {
|
|
||||||
const [dialogState, setDialogState] = useState<{
|
|
||||||
open: boolean;
|
|
||||||
props: Omit<ConfirmDialogProps, 'open' | 'onOpenChange'>;
|
|
||||||
}>({
|
|
||||||
open: false,
|
|
||||||
props: {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
onConfirm: () => {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const showConfirm = (props: Omit<ConfirmDialogProps, 'open' | 'onOpenChange'>) => {
|
|
||||||
setDialogState({ open: true, props });
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogProps: ConfirmDialogProps = {
|
|
||||||
open: dialogState.open,
|
|
||||||
onOpenChange: (open: boolean) => setDialogState(prev => ({ ...prev, open })),
|
|
||||||
...dialogState.props,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
dialogProps,
|
|
||||||
showConfirm,
|
|
||||||
ConfirmDialogComponent: <ConfirmDialog {...dialogProps} />,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConfirmDialog;
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Target, FlaskConical, Sparkles, Play, Pause, X } from 'lucide-react';
|
|
||||||
import { fmt } from '@/lib/game/stores';
|
|
||||||
import { formatStudyTime } from '@/lib/game/utils/formatting';
|
|
||||||
import type { EquipmentInstance, EnchantmentDesign } from '@/lib/game/types';
|
|
||||||
|
|
||||||
interface CraftingProgressProps {
|
|
||||||
designProgress: { designId: string; progress: number; required: number } | null;
|
|
||||||
preparationProgress: { equipmentInstanceId: string; progress: number; required: number; manaCostPaid: number } | null;
|
|
||||||
applicationProgress: { equipmentInstanceId: string; designId: string; progress: number; required: number; manaPerHour: number; paused: boolean } | null;
|
|
||||||
equipmentInstances: Record<string, EquipmentInstance>;
|
|
||||||
enchantmentDesigns: EnchantmentDesign[];
|
|
||||||
cancelDesign: () => void;
|
|
||||||
cancelPreparation: () => void;
|
|
||||||
pauseApplication: () => void;
|
|
||||||
resumeApplication: () => void;
|
|
||||||
cancelApplication: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CraftingProgress({
|
|
||||||
designProgress,
|
|
||||||
preparationProgress,
|
|
||||||
applicationProgress,
|
|
||||||
equipmentInstances,
|
|
||||||
enchantmentDesigns,
|
|
||||||
cancelDesign,
|
|
||||||
cancelPreparation,
|
|
||||||
pauseApplication,
|
|
||||||
resumeApplication,
|
|
||||||
cancelApplication,
|
|
||||||
}: CraftingProgressProps) {
|
|
||||||
const progressSections: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
// Design progress
|
|
||||||
if (designProgress) {
|
|
||||||
const progressPct = Math.min(100, (designProgress.progress / designProgress.required) * 100);
|
|
||||||
progressSections.push(
|
|
||||||
<div key="design" 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">
|
|
||||||
<Target className="w-4 h-4 text-cyan-400" />
|
|
||||||
<span className="text-sm font-semibold text-cyan-300">
|
|
||||||
Designing Enchantment
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={cancelDesign}
|
|
||||||
>
|
|
||||||
<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(designProgress.progress)} / {formatStudyTime(designProgress.required)}</span>
|
|
||||||
<span>Design Time</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preparation progress
|
|
||||||
if (preparationProgress) {
|
|
||||||
const progressPct = Math.min(100, (preparationProgress.progress / preparationProgress.required) * 100);
|
|
||||||
const instance = equipmentInstances[preparationProgress.equipmentInstanceId];
|
|
||||||
progressSections.push(
|
|
||||||
<div key="prepare" className="p-3 rounded border border-green-600/50 bg-green-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FlaskConical className="w-4 h-4 text-green-400" />
|
|
||||||
<span className="text-sm font-semibold text-green-300">
|
|
||||||
Preparing {instance?.name || 'Equipment'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={cancelPreparation}
|
|
||||||
>
|
|
||||||
<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(preparationProgress.progress)} / {formatStudyTime(preparationProgress.required)}</span>
|
|
||||||
<span>Mana spent: {fmt(preparationProgress.manaCostPaid)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application progress
|
|
||||||
if (applicationProgress) {
|
|
||||||
const progressPct = Math.min(100, (applicationProgress.progress / applicationProgress.required) * 100);
|
|
||||||
const instance = equipmentInstances[applicationProgress.equipmentInstanceId];
|
|
||||||
const design = enchantmentDesigns.find(d => d.id === applicationProgress.designId);
|
|
||||||
progressSections.push(
|
|
||||||
<div key="enchant" className="p-3 rounded border border-amber-600/50 bg-amber-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Sparkles className="w-4 h-4 text-amber-400" />
|
|
||||||
<span className="text-sm font-semibold text-amber-300">
|
|
||||||
Enchanting {instance?.name || 'Equipment'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{applicationProgress.paused ? (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-green-400 hover:text-green-300"
|
|
||||||
onClick={resumeApplication}
|
|
||||||
>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-yellow-400 hover:text-yellow-300"
|
|
||||||
onClick={pauseApplication}
|
|
||||||
>
|
|
||||||
<Pause className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={cancelApplication}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(applicationProgress.progress)} / {formatStudyTime(applicationProgress.required)}</span>
|
|
||||||
<span>Mana/hr: {fmt(applicationProgress.manaPerHour)}</span>
|
|
||||||
</div>
|
|
||||||
{design && (
|
|
||||||
<div className="text-xs text-amber-400/70 mt-1">
|
|
||||||
Applying: {design.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return progressSections.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{progressSections}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
CraftingProgress.displayName = "CraftingProgress";
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import {
|
|
||||||
Gem, Search, ArrowUpDown, AlertTriangle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { ElementBadge } from '@/components/ui/element-badge';
|
|
||||||
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
|
||||||
import { LOOT_DROPS } from '@/lib/game/data/loot-drops';
|
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { useGameToast } from '@/components/game/GameToast';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
|
|
||||||
import { type SortMode, type FilterMode, RARITY_ORDER } from './types';
|
|
||||||
import { MaterialsSection } from './MaterialItem';
|
|
||||||
import { EssenceSection } from './EssenceItem';
|
|
||||||
import { BlueprintsSection } from './BlueprintsSection';
|
|
||||||
import { EquipmentSection } from './EquipmentItem';
|
|
||||||
|
|
||||||
interface LootInventoryProps {
|
|
||||||
inventory: LootInventoryType;
|
|
||||||
elements?: Record<string, ElementState>;
|
|
||||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
|
||||||
onDeleteMaterial?: (materialId: string, amount: number) => void;
|
|
||||||
onDeleteEquipment?: (instanceId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LootInventoryDisplay({
|
|
||||||
inventory,
|
|
||||||
elements,
|
|
||||||
equipmentInstances = {},
|
|
||||||
onDeleteMaterial,
|
|
||||||
onDeleteEquipment,
|
|
||||||
}: LootInventoryProps) {
|
|
||||||
const showToast = useGameToast();
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [sortMode, setSortMode] = useState<SortMode>('rarity');
|
|
||||||
const [filterMode, setFilterMode] = useState<FilterMode>('all');
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
|
|
||||||
|
|
||||||
// Count items
|
|
||||||
const materialCount = Object.values(inventory.materials || {}).reduce((a, b) => a + b, 0);
|
|
||||||
const essenceCount = elements ? Object.entries(elements).reduce((a, [id, e]) => id === 'transference' ? a : a + e.current, 0) : 0;
|
|
||||||
const blueprintCount = inventory.blueprints.length;
|
|
||||||
const equipmentCount = Object.keys(equipmentInstances).length;
|
|
||||||
const totalItems = materialCount + blueprintCount + equipmentCount;
|
|
||||||
|
|
||||||
// Filter and sort materials
|
|
||||||
const filteredMaterials = Object.entries(inventory.materials)
|
|
||||||
.filter(([id, count]) => {
|
|
||||||
if (count <= 0) return false;
|
|
||||||
const drop = LOOT_DROPS[id];
|
|
||||||
if (!drop) return false;
|
|
||||||
if (searchTerm && !drop.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aCount], [bId, bCount]) => {
|
|
||||||
const aDrop = LOOT_DROPS[aId];
|
|
||||||
const bDrop = LOOT_DROPS[bId];
|
|
||||||
if (!aDrop || !bDrop) return 0;
|
|
||||||
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return aDrop.name.localeCompare(bDrop.name);
|
|
||||||
case 'rarity':
|
|
||||||
return RARITY_ORDER[bDrop.rarity] - RARITY_ORDER[aDrop.rarity];
|
|
||||||
case 'count':
|
|
||||||
return bCount - aCount;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter and sort essence
|
|
||||||
const filteredEssence = elements
|
|
||||||
? Object.entries(elements)
|
|
||||||
.filter(([id, state]) => {
|
|
||||||
if (!state.unlocked || state.current <= 0) return false;
|
|
||||||
if (id === 'transference') return false;
|
|
||||||
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aState], [bId, bState]) => {
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return (ELEMENTS[aId]?.name || aId).localeCompare(ELEMENTS[bId]?.name || bId);
|
|
||||||
case 'count':
|
|
||||||
return bState.current - aState.current;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Filter and sort equipment
|
|
||||||
const filteredEquipment = Object.entries(equipmentInstances)
|
|
||||||
.filter(([id, instance]) => {
|
|
||||||
if (searchTerm && !instance.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aInst], [bId, bInst]) => {
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return aInst.name.localeCompare(bInst.name);
|
|
||||||
case 'rarity':
|
|
||||||
return RARITY_ORDER[bInst.rarity] - RARITY_ORDER[aInst.rarity];
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if we have anything to show
|
|
||||||
const hasItems = totalItems > 0 || essenceCount > 0;
|
|
||||||
|
|
||||||
const handleDeleteMaterial = (materialId: string) => {
|
|
||||||
const drop = LOOT_DROPS[materialId];
|
|
||||||
if (drop) {
|
|
||||||
setDeleteConfirm({ type: 'material', id: materialId, name: drop.name });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteEquipment = (instanceId: string) => {
|
|
||||||
const instance = equipmentInstances[instanceId];
|
|
||||||
if (instance) {
|
|
||||||
setDeleteConfirm({ type: 'equipment', id: instanceId, name: instance.name });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (!deleteConfirm) return;
|
|
||||||
|
|
||||||
if (deleteConfirm.type === 'material' && onDeleteMaterial) {
|
|
||||||
const amount = inventory.materials[deleteConfirm.id] || 0;
|
|
||||||
onDeleteMaterial(deleteConfirm.id, amount);
|
|
||||||
showToast('success', 'Material Deleted', `${deleteConfirm.name} removed from inventory`);
|
|
||||||
} else if (deleteConfirm.type === 'equipment' && onDeleteEquipment) {
|
|
||||||
onDeleteEquipment(deleteConfirm.id);
|
|
||||||
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeleteConfirm(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 */}
|
|
||||||
<div className="flex gap-2 mb-3">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--text-muted)]" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="h-7 pl-7 bg-[var(--bg-sunken)] border-[var(--border-subtle)] text-xs text-[var(--text-primary)] placeholder:text-[var(--text-disabled)]"
|
|
||||||
aria-label="Search inventory"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActionButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-2"
|
|
||||||
onClick={() => setSortMode(sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity')}
|
|
||||||
aria-label={`Sort by ${sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity'}`}
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
|
||||||
<div className="flex gap-1 flex-wrap mb-3">
|
|
||||||
{[
|
|
||||||
{ mode: 'all' as FilterMode, label: 'All' },
|
|
||||||
{ mode: 'materials' as FilterMode, label: `Materials (${materialCount})` },
|
|
||||||
{ mode: 'essence' as FilterMode, label: `Essence (${essenceCount})` },
|
|
||||||
{ mode: 'blueprints' as FilterMode, label: `Blueprints (${blueprintCount})` },
|
|
||||||
{ mode: 'equipment' as FilterMode, label: `Equipment (${equipmentCount})` },
|
|
||||||
].map(({ mode, label }) => (
|
|
||||||
<ActionButton
|
|
||||||
key={mode}
|
|
||||||
variant={filterMode === mode ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
className={`h-6 px-2 text-xs ${filterMode === mode ? '' : 'bg-[var(--bg-sunken)]'}`}
|
|
||||||
onClick={() => setFilterMode(mode)}
|
|
||||||
aria-pressed={filterMode === mode}
|
|
||||||
aria-label={`Filter by ${label}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</ActionButton>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="bg-[var(--border-subtle)] mb-3" />
|
|
||||||
|
|
||||||
<ScrollArea className="h-64 w-full">
|
|
||||||
<div className="space-y-3 pr-2">
|
|
||||||
{/* Materials */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'materials') && (
|
|
||||||
<MaterialsSection
|
|
||||||
materials={filteredMaterials}
|
|
||||||
onDeleteMaterial={handleDeleteMaterial}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Essence */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'essence') && (
|
|
||||||
<EssenceSection essence={filteredEssence} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Blueprints */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'blueprints') && (
|
|
||||||
<BlueprintsSection blueprints={inventory.blueprints} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Equipment */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'equipment') && (
|
|
||||||
<EquipmentSection
|
|
||||||
equipment={filteredEquipment}
|
|
||||||
onDeleteEquipment={handleDeleteEquipment}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
<AlertDialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
|
||||||
<AlertDialogContent className="bg-[var(--bg-surface)] border-[var(--border-default)]">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="text-[var(--mana-light)] flex items-center gap-2">
|
|
||||||
<AlertTriangle className="w-5 h-5" />
|
|
||||||
Delete Item
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-[var(--text-secondary)]">
|
|
||||||
Are you sure you want to delete <strong className="text-[var(--text-primary)]">{deleteConfirm?.name}</strong>?
|
|
||||||
{deleteConfirm?.type === 'material' && (
|
|
||||||
<span className="block mt-2 text-[var(--color-danger)]">
|
|
||||||
This will delete ALL {inventory.materials[deleteConfirm?.id || ''] || 0} of this material!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{deleteConfirm?.type === 'equipment' && (
|
|
||||||
<span className="block mt-2 text-[var(--color-danger)]">
|
|
||||||
This equipment and all its enchantments will be permanently lost!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel className="bg-[var(--bg-sunken)] border-[var(--border-default)] text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]">
|
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className="bg-[var(--interactive-danger)] hover:bg-[var(--interactive-danger-hover)] text-white"
|
|
||||||
onClick={confirmDelete}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LootInventoryDisplay.displayName = "LootInventoryDisplay";
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import {
|
|
||||||
Gem, Search, ArrowUpDown, AlertTriangle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { ElementBadge } from '@/components/ui/element-badge';
|
|
||||||
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
|
||||||
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { useGameToast } from '@/components/game/GameToast';
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
|
|
||||||
import { type SortMode, type FilterMode, RARITY_ORDER, RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
|
||||||
import { MaterialsSection } from './MaterialItem';
|
|
||||||
import { EssenceSection } from './EssenceItem';
|
|
||||||
import { BlueprintsSection } from './BlueprintsSection';
|
|
||||||
import { EquipmentSection } from './EquipmentItem';
|
|
||||||
|
|
||||||
interface LootInventoryProps {
|
|
||||||
inventory: LootInventoryType;
|
|
||||||
elements?: Record<string, ElementState>;
|
|
||||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
|
||||||
onDeleteMaterial?: (materialId: string, amount: number) => void;
|
|
||||||
onDeleteEquipment?: (instanceId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LootInventoryDisplay({
|
|
||||||
inventory,
|
|
||||||
elements,
|
|
||||||
equipmentInstances = {},
|
|
||||||
onDeleteMaterial,
|
|
||||||
onDeleteEquipment,
|
|
||||||
}: LootInventoryProps) {
|
|
||||||
const showToast = useGameToast();
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [sortMode, setSortMode] = useState<SortMode>('rarity');
|
|
||||||
const [filterMode, setFilterMode] = useState<FilterMode>('all');
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
|
|
||||||
|
|
||||||
// Count items
|
|
||||||
const materialCount = Object.values(inventory.materials || {}).reduce((a: number, b: number) => a + b, 0);
|
|
||||||
|
|
||||||
// Calculate essence count
|
|
||||||
let essenceCount = 0;
|
|
||||||
if (elements) {
|
|
||||||
essenceCount = Object.entries(elements).reduce((acc: number, [id, state]) => {
|
|
||||||
if (id === 'transference') return acc;
|
|
||||||
return acc + (state.current || 0);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blueprintCount = inventory.blueprints.length;
|
|
||||||
const equipmentCount = Object.keys(equipmentInstances).length;
|
|
||||||
const totalItems = materialCount + blueprintCount + equipmentCount;
|
|
||||||
|
|
||||||
// Filter and sort materials
|
|
||||||
const filteredMaterials = Object.entries(inventory.materials)
|
|
||||||
.filter(([id, count]) => {
|
|
||||||
if (count <= 0) return false;
|
|
||||||
const drop = LOOT_DROPS[id];
|
|
||||||
if (!drop) return false;
|
|
||||||
if (searchTerm && !drop.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aCount], [bId, bCount]) => {
|
|
||||||
const aDrop = LOOT_DROPS[aId];
|
|
||||||
const bDrop = LOOT_DROPS[bId];
|
|
||||||
if (!aDrop || !bDrop) return 0;
|
|
||||||
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return aDrop.name.localeCompare(bDrop.name);
|
|
||||||
case 'rarity':
|
|
||||||
return RARITY_ORDER[bDrop.rarity] - RARITY_ORDER[aDrop.rarity];
|
|
||||||
case 'count':
|
|
||||||
return bCount - aCount;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter and sort essence
|
|
||||||
const filteredEssence = elements
|
|
||||||
? Object.entries(elements)
|
|
||||||
.filter(([id, state]) => {
|
|
||||||
if (!state.unlocked || state.current <= 0) return false;
|
|
||||||
if (id === 'transference') return false;
|
|
||||||
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aState], [bId, bState]) => {
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return (ELEMENTS[aId]?.name || aId).localeCompare(ELEMENTS[bId]?.name || bId);
|
|
||||||
case 'count':
|
|
||||||
return bState.current - aState.current;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Filter and sort equipment
|
|
||||||
const filteredEquipment = Object.entries(equipmentInstances)
|
|
||||||
.filter(([id, instance]) => {
|
|
||||||
if (searchTerm && !instance.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sort(([aId, aInst], [bId, bInst]) => {
|
|
||||||
switch (sortMode) {
|
|
||||||
case 'name':
|
|
||||||
return aInst.name.localeCompare(bInst.name);
|
|
||||||
case 'rarity':
|
|
||||||
return RARITY_ORDER[bInst.rarity] - RARITY_ORDER[aInst.rarity];
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasItems = totalItems > 0 || essenceCount > 0;
|
|
||||||
|
|
||||||
const handleDeleteMaterial = (materialId: string) => {
|
|
||||||
const drop = LOOT_DROPS[materialId];
|
|
||||||
if (drop) {
|
|
||||||
setDeleteConfirm({ type: 'material', id: materialId, name: drop.name });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteEquipment = (instanceId: string) => {
|
|
||||||
const instance = equipmentInstances[instanceId];
|
|
||||||
if (instance) {
|
|
||||||
setDeleteConfirm({ type: 'equipment', id: instanceId, name: instance.name });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (!deleteConfirm) return;
|
|
||||||
|
|
||||||
if (deleteConfirm.type === 'material' && onDeleteMaterial) {
|
|
||||||
const amount = inventory.materials[deleteConfirm.id] || 0;
|
|
||||||
onDeleteMaterial(deleteConfirm.id, amount);
|
|
||||||
showToast('success', 'Material Deleted', `${deleteConfirm.name} removed from inventory`);
|
|
||||||
} else if (deleteConfirm.type === 'equipment' && onDeleteEquipment) {
|
|
||||||
onDeleteEquipment(deleteConfirm.id);
|
|
||||||
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeleteConfirm(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 */}
|
|
||||||
<div className="flex gap-2 mb-3">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--text-muted)]" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="h-7 pl-7 bg-[var(--bg-sunken)] border-[var(--border-subtle)] text-xs text-[var(--text-primary)] placeholder:text-[var(--text-disabled)]"
|
|
||||||
aria-label="Search inventory"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActionButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-2"
|
|
||||||
onClick={() => setSortMode(sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity')}
|
|
||||||
aria-label={`Sort by ${sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity'}`}
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
|
||||||
<div className="flex gap-1 flex-wrap mb-3">
|
|
||||||
{[
|
|
||||||
{ mode: 'all' as FilterMode, label: 'All' },
|
|
||||||
{ mode: 'materials' as FilterMode, label: `Materials (${materialCount})` },
|
|
||||||
{ mode: 'essence' as FilterMode, label: `Essence (${essenceCount})` },
|
|
||||||
{ mode: 'blueprints' as FilterMode, label: `Blueprints (${blueprintCount})` },
|
|
||||||
{ mode: 'equipment' as FilterMode, label: `Equipment (${equipmentCount})` },
|
|
||||||
].map(({ mode, label }) => (
|
|
||||||
<ActionButton
|
|
||||||
key={mode}
|
|
||||||
variant={filterMode === mode ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
className={`h-6 px-2 text-xs ${filterMode === mode ? '' : 'bg-[var(--bg-sunken)]'}`}
|
|
||||||
onClick={() => setFilterMode(mode)}
|
|
||||||
aria-pressed={filterMode === mode}
|
|
||||||
aria-label={`Filter by ${label}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</ActionButton>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="bg-[var(--border-subtle)] mb-3" />
|
|
||||||
|
|
||||||
<ScrollArea className="h-64 w-full">
|
|
||||||
<div className="space-y-3 pr-2">
|
|
||||||
{/* Materials */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'materials') && (
|
|
||||||
<MaterialsSection
|
|
||||||
materials={filteredMaterials}
|
|
||||||
onDeleteMaterial={handleDeleteMaterial}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Essence */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'essence') && (
|
|
||||||
<EssenceSection essence={filteredEssence} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Blueprints */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'blueprints') && (
|
|
||||||
<BlueprintsSection blueprints={inventory.blueprints} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Equipment */}
|
|
||||||
{(filterMode === 'all' || filterMode === 'equipment') && (
|
|
||||||
<EquipmentSection
|
|
||||||
equipment={filteredEquipment}
|
|
||||||
onDeleteEquipment={handleDeleteEquipment}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
<AlertDialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
|
||||||
<AlertDialogContent className="bg-[var(--bg-surface)] border-[var(--border-default)]">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="text-[var(--mana-light)] flex items-center gap-2">
|
|
||||||
<AlertTriangle className="w-5 h-5" />
|
|
||||||
Delete Item
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-[var(--text-secondary)]">
|
|
||||||
Are you sure you want to delete <strong className="text-[var(--text-primary)]">{deleteConfirm?.name}</strong>?
|
|
||||||
{deleteConfirm?.type === 'material' && (
|
|
||||||
<span className="block mt-2 text-[var(--color-danger)]">
|
|
||||||
This will delete ALL {inventory.materials[deleteConfirm?.id || ''] || 0} of this material!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{deleteConfirm?.type === 'equipment' && (
|
|
||||||
<span className="block mt-2 text-[var(--color-danger)]">
|
|
||||||
This equipment and all its enchantments will be permanently lost!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel className="bg-[var(--bg-sunken)] border-[var(--border-default)] text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]">
|
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className="bg-[var(--interactive-danger)] hover:bg-[var(--interactive-danger-hover)] text-white"
|
|
||||||
onClick={confirmDelete}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LootInventoryDisplay.displayName = "LootInventoryDisplay";
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { BookOpen, X } from 'lucide-react';
|
|
||||||
import { SPELLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { formatStudyTime } from '@/lib/game/utils/formatting';
|
|
||||||
import type { StudyTarget } from '@/lib/game/types';
|
|
||||||
|
|
||||||
interface StudyProgressProps {
|
|
||||||
currentStudyTarget: StudyTarget | null;
|
|
||||||
skills: Record<string, number>;
|
|
||||||
studySpeedMult: number;
|
|
||||||
cancelStudy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StudyProgress({
|
|
||||||
currentStudyTarget,
|
|
||||||
skills,
|
|
||||||
studySpeedMult,
|
|
||||||
cancelStudy,
|
|
||||||
}: StudyProgressProps) {
|
|
||||||
if (!currentStudyTarget) return null;
|
|
||||||
|
|
||||||
const target = currentStudyTarget;
|
|
||||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
|
||||||
const isSkill = target.type === 'skill';
|
|
||||||
const def = isSkill ? undefined : SPELLS_DEF[target.id];
|
|
||||||
const currentLevel = isSkill ? (skills[target.id] || 0) : 0;
|
|
||||||
|
|
||||||
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 ?? target.id}
|
|
||||||
{isSkill && ` Lv.${currentLevel + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={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>{studySpeedMult.toFixed(1)}x speed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StudyProgress.displayName = "StudyProgress";
|
|
||||||
@@ -7,9 +7,6 @@ export { StatsTab } from './StatsTab';
|
|||||||
|
|
||||||
// UI components
|
// UI components
|
||||||
export { ActionButtons } from './ActionButtons';
|
export { ActionButtons } from './ActionButtons';
|
||||||
export { CalendarDisplay } from './CalendarDisplay';
|
|
||||||
export { CraftingProgress } from './CraftingProgress';
|
|
||||||
export { StudyProgress } from './StudyProgress';
|
|
||||||
export { ManaDisplay } from './ManaDisplay';
|
export { ManaDisplay } from './ManaDisplay';
|
||||||
export { TimeDisplay } from './TimeDisplay';
|
export { TimeDisplay } from './TimeDisplay';
|
||||||
export { UpgradeDialog } from './UpgradeDialog';
|
export { UpgradeDialog } from './UpgradeDialog';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
||||||
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
||||||
@@ -33,6 +33,10 @@ interface DisciplineCardProps {
|
|||||||
drainBase: number;
|
drainBase: number;
|
||||||
difficultyFactor: number;
|
difficultyFactor: number;
|
||||||
scalingFactor: number;
|
scalingFactor: number;
|
||||||
|
xp: number;
|
||||||
|
paused: boolean;
|
||||||
|
concurrentLimit: number;
|
||||||
|
onToggle: (id: string, paused: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
||||||
@@ -47,14 +51,14 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
|||||||
drainBase,
|
drainBase,
|
||||||
difficultyFactor,
|
difficultyFactor,
|
||||||
scalingFactor,
|
scalingFactor,
|
||||||
|
xp,
|
||||||
|
paused,
|
||||||
|
concurrentLimit,
|
||||||
|
onToggle,
|
||||||
}) => {
|
}) => {
|
||||||
const activeIds = useDisciplineStore((s) => s.activeIds);
|
const displayXp = xp;
|
||||||
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
|
||||||
const currentDisc = useDisciplineStore((s) => s.disciplines[id] ?? { xp: 0, paused: true });
|
|
||||||
|
|
||||||
const displayXp = currentDisc.xp;
|
|
||||||
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
|
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
|
||||||
const isPaused = currentDisc.paused;
|
const isPaused = paused;
|
||||||
|
|
||||||
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
|
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
|
||||||
const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor);
|
const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor);
|
||||||
@@ -73,11 +77,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleAction = () => {
|
const toggleAction = () => {
|
||||||
if (isPaused) {
|
onToggle(id, isPaused);
|
||||||
useDisciplineStore.getState().activate(id);
|
|
||||||
} else {
|
|
||||||
useDisciplineStore.getState().deactivate(id);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -135,7 +135,11 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DisciplinesTab: React.FC = () => {
|
export const DisciplinesTab: React.FC = () => {
|
||||||
const { activeIds, concurrentLimit } = useDisciplineStore();
|
const activeIds = useDisciplineStore((s) => s.activeIds);
|
||||||
|
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
||||||
|
const disciplines = useDisciplineStore((s) => s.disciplines);
|
||||||
|
const activate = useDisciplineStore((s) => s.activate);
|
||||||
|
const deactivate = useDisciplineStore((s) => s.deactivate);
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
||||||
@@ -144,6 +148,14 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleToggle = useCallback((id: string, paused: boolean) => {
|
||||||
|
if (paused) {
|
||||||
|
activate(id);
|
||||||
|
} else {
|
||||||
|
deactivate(id);
|
||||||
|
}
|
||||||
|
}, [activate, deactivate]);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||||
@@ -177,22 +189,29 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
|
|
||||||
{/* Discipline cards — only render active tab */}
|
{/* Discipline cards — only render active tab */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{activeTab?.items.map((disc) => (
|
{activeTab?.items.map((disc) => {
|
||||||
<DisciplineCard
|
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
||||||
key={disc.id}
|
return (
|
||||||
id={disc.id}
|
<DisciplineCard
|
||||||
name={disc.name}
|
key={disc.id}
|
||||||
description={disc.description}
|
id={disc.id}
|
||||||
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
name={disc.name}
|
||||||
perkValues={disc.perks?.map((p) => p.value)}
|
description={disc.description}
|
||||||
perkTypes={disc.perks?.map((p) => p.type)}
|
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
||||||
statBonus={disc.statBonus.stat}
|
perkValues={disc.perks?.map((p) => p.value)}
|
||||||
baseValue={disc.statBonus.baseValue}
|
perkTypes={disc.perks?.map((p) => p.type)}
|
||||||
drainBase={disc.drainBase}
|
statBonus={disc.statBonus.stat}
|
||||||
difficultyFactor={disc.difficultyFactor}
|
baseValue={disc.statBonus.baseValue}
|
||||||
scalingFactor={disc.scalingFactor}
|
drainBase={disc.drainBase}
|
||||||
/>
|
difficultyFactor={disc.difficultyFactor}
|
||||||
))}
|
scalingFactor={disc.scalingFactor}
|
||||||
|
xp={discState.xp}
|
||||||
|
paused={discState.paused}
|
||||||
|
concurrentLimit={concurrentLimit}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary info */}
|
{/* Summary info */}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function equipItem(
|
|||||||
const instance = state.equipmentInstances[instanceId];
|
const instance = state.equipmentInstances[instanceId];
|
||||||
if (!instance) return false;
|
if (!instance) return false;
|
||||||
|
|
||||||
if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances)) {
|
if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances, state.equipmentInstances)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ export function refundCraftMaterials(recipe: CraftingRecipe, refundRate: number
|
|||||||
export function canEquipInSlot(
|
export function canEquipInSlot(
|
||||||
instance: EquipmentInstance,
|
instance: EquipmentInstance,
|
||||||
slot: EquipmentSlot,
|
slot: EquipmentSlot,
|
||||||
currentlyEquipped: Record<EquipmentSlot, string | null>
|
currentlyEquipped: Record<EquipmentSlot, string | null>,
|
||||||
|
instances: Record<string, EquipmentInstance> = {},
|
||||||
): boolean {
|
): boolean {
|
||||||
const type = EQUIPMENT_TYPES[instance.typeId];
|
const type = EQUIPMENT_TYPES[instance.typeId];
|
||||||
if (!type) return false;
|
if (!type) return false;
|
||||||
@@ -145,7 +146,8 @@ export function canEquipInSlot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (slot === 'offHand' && currentlyEquipped.mainHand) {
|
if (slot === 'offHand' && currentlyEquipped.mainHand) {
|
||||||
const mainHandType = EQUIPMENT_TYPES[currentlyEquipped.mainHand];
|
const mainHandInstance = instances[currentlyEquipped.mainHand];
|
||||||
|
const mainHandType = mainHandInstance ? EQUIPMENT_TYPES[mainHandInstance.typeId] : undefined;
|
||||||
if (mainHandType?.twoHanded) {
|
if (mainHandType?.twoHanded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
type DisciplineDefinition = {
|
|
||||||
name: string;
|
|
||||||
attunement: DisciplinesAttunementType;
|
|
||||||
manaType: ManaType;
|
|
||||||
baseCost: number;
|
|
||||||
description: string;
|
|
||||||
requires?: DisciplineDefinition[];
|
|
||||||
};
|
|
||||||
|
|
||||||
enum DisciplinesAttunementType {
|
|
||||||
base,
|
|
||||||
enchanter,
|
|
||||||
fabricator,
|
|
||||||
invoker
|
|
||||||
};
|
|
||||||
|
|
||||||
export const baseDisciplines: DisciplineDefinition[] = [
|
|
||||||
{
|
|
||||||
name: "Embercraft",
|
|
||||||
attunement: DisciplinesAttunementType.base,
|
|
||||||
manaType: "fire",
|
|
||||||
baseCost: 10,
|
|
||||||
description: "Basic flame projection with autocrit on combustion explosion",
|
|
||||||
requires: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Earthbind",
|
|
||||||
attunement: DisciplinesAttunementType.base,
|
|
||||||
manaType: "earth",
|
|
||||||
baseCost: 12,
|
|
||||||
description: "Basic mana chains with passive ground stability",
|
|
||||||
requires: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
type DisciplineDefinition = {
|
|
||||||
name: string;
|
|
||||||
attunement: DisciplinesAttunementType;
|
|
||||||
manaType: ManaType;
|
|
||||||
baseCost: number;
|
|
||||||
description: string;
|
|
||||||
requires?: DisciplineDefinition[];
|
|
||||||
};
|
|
||||||
|
|
||||||
enum DisciplinesAttunementType {
|
|
||||||
base,
|
|
||||||
enchanter,
|
|
||||||
fabricator,
|
|
||||||
invoker
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enchanterDisciplines: DisciplineDefinition[] = [
|
|
||||||
{
|
|
||||||
name: "Soulforge",
|
|
||||||
attunement: DisciplinesAttunementType.enchanter,
|
|
||||||
manaType: "light",
|
|
||||||
baseCost: 25,
|
|
||||||
description: "Mana chains that create permanent elemental storage nodes",
|
|
||||||
requires: [{name: "Embercraft"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mana Prism",
|
|
||||||
attunement: DisciplinesAttunementType.enchanter,
|
|
||||||
manaType: "light",
|
|
||||||
baseCost: 30,
|
|
||||||
description: "Prismatic mana focusing that reflexes attacks as fixed ratio",
|
|
||||||
requires: [{name: "Soulforge"}]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
|
||||||
|
|
||||||
const fabricatorDisciplines: DisciplineDefinition[] = [
|
|
||||||
{
|
|
||||||
name: 'Metalworking',
|
|
||||||
attunement: 'fabricator',
|
|
||||||
manaType: 'metal',
|
|
||||||
baseCosts: { mana: 28, time: 7 },
|
|
||||||
description: 'Increase metal equipment crafting speed',
|
|
||||||
thresholds: { xp: 140, interval: 70 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Crystal Shaping',
|
|
||||||
attunement: 'fabricator',
|
|
||||||
manaType: 'crystal',
|
|
||||||
baseCosts: { mana: 30, time: 8 },
|
|
||||||
description: 'Increase crystal equipment durability',
|
|
||||||
thresholds: { xp: 160, interval: 80 }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
export default fabricatorDisciplines;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
|
||||||
|
|
||||||
const invokerDisciplines: DisciplineDefinition[] = [
|
|
||||||
{
|
|
||||||
name: 'Lightning Surge',
|
|
||||||
attunement: 'invoker',
|
|
||||||
manaType: 'lightning',
|
|
||||||
baseCost: 30,
|
|
||||||
description: 'Boost lightning spell damage',
|
|
||||||
thresholds: { xp: 150, interval: 75 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Void Echo',
|
|
||||||
attunement: 'invoker',
|
|
||||||
manaType: 'void',
|
|
||||||
baseCost: 35,
|
|
||||||
description: 'Increase void spell cast speed',
|
|
||||||
thresholds: { xp: 180, interval: 90 }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
export default invokerDisciplines;
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
// Import types
|
// Import types
|
||||||
import type { EnchantmentEffectCategory, EnchantmentEffectDef } from '../enchantment-types'
|
import type { EnchantmentEffectCategory, EnchantmentEffectDef } from '../enchantment-types'
|
||||||
|
import type { EquipmentCategory } from '../equipment'
|
||||||
|
|
||||||
// Import all category-specific effect collections
|
// Import all category-specific effect collections
|
||||||
import { SPELL_EFFECTS } from './spell-effects'
|
import { SPELL_EFFECTS } from './spell-effects'
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
// ─── Upgrade Effect Types ────────────────────────────────────────────────────
|
// ─── Upgrade Effect Types ────────────────────────────────────────────────────
|
||||||
// Type interfaces for upgrade effects
|
// Type interfaces for upgrade effects
|
||||||
|
|
||||||
import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types';
|
|
||||||
|
|
||||||
export interface ActiveUpgradeEffect {
|
export interface ActiveUpgradeEffect {
|
||||||
upgradeId: string;
|
upgradeId: string;
|
||||||
skillId: string;
|
skillId: string;
|
||||||
milestone: 5 | 10;
|
milestone: 5 | 10;
|
||||||
effect: SkillUpgradeEffect;
|
effect: string;
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
}
|
}
|
||||||
@@ -23,7 +21,7 @@ export interface ComputedEffects {
|
|||||||
meditationEfficiency: number;
|
meditationEfficiency: number;
|
||||||
spellCostMultiplier: number;
|
spellCostMultiplier: number;
|
||||||
conversionEfficiency: number;
|
conversionEfficiency: number;
|
||||||
|
|
||||||
// Combat effects
|
// Combat effects
|
||||||
baseDamageMultiplier: number;
|
baseDamageMultiplier: number;
|
||||||
baseDamageBonus: number;
|
baseDamageBonus: number;
|
||||||
@@ -31,33 +29,33 @@ export interface ComputedEffects {
|
|||||||
critChanceBonus: number;
|
critChanceBonus: number;
|
||||||
critDamageMultiplier: number;
|
critDamageMultiplier: number;
|
||||||
elementalDamageMultiplier: number;
|
elementalDamageMultiplier: number;
|
||||||
|
|
||||||
// Study effects
|
// Study effects
|
||||||
studySpeedMultiplier: number;
|
studySpeedMultiplier: number;
|
||||||
studyCostMultiplier: number;
|
studyCostMultiplier: number;
|
||||||
progressRetention: number;
|
progressRetention: number;
|
||||||
instantStudyChance: number;
|
instantStudyChance: number;
|
||||||
freeStudyChance: number;
|
freeStudyChance: number;
|
||||||
|
|
||||||
// Element effects
|
// Element effects
|
||||||
elementCapMultiplier: number;
|
elementCapMultiplier: number;
|
||||||
elementCapBonus: number;
|
elementCapBonus: number;
|
||||||
perElementCapBonus: Record<string, number>;
|
perElementCapBonus: Record<string, number>;
|
||||||
conversionCostMultiplier: number;
|
conversionCostMultiplier: number;
|
||||||
doubleCraftChance: number;
|
doubleCraftChance: number;
|
||||||
|
|
||||||
// Special values
|
// Special values
|
||||||
permanentRegenBonus: number;
|
permanentRegenBonus: number;
|
||||||
|
|
||||||
// Special effect flags
|
// Special effect flags
|
||||||
specials: Set<string>;
|
specials: Set<string>;
|
||||||
|
|
||||||
// All active upgrades for display
|
// All active upgrades for display
|
||||||
activeUpgrades: ActiveUpgradeEffect[];
|
activeUpgrades: ActiveUpgradeEffect[];
|
||||||
|
|
||||||
// DEEP_UNDERSTANDING: +10% bonus from all skill levels
|
// DEEP_UNDERSTANDING: +10% bonus from all skill levels
|
||||||
skillLevelMultiplier: number;
|
skillLevelMultiplier: number;
|
||||||
|
|
||||||
// Enchantment Power
|
// Enchantment Power
|
||||||
enchantmentPowerMultiplier: number;
|
enchantmentPowerMultiplier: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ export function processCombatTick(
|
|||||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||||
rawMana = afterCost.rawMana;
|
rawMana = afterCost.rawMana;
|
||||||
elements = afterCost.elements;
|
elements = afterCost.elements;
|
||||||
totalManaGathered += spellDef.cost.amount;
|
|
||||||
|
|
||||||
// Calculate base damage
|
// Calculate base damage
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
const damage = calcDamage(
|
const damage = calcDamage(
|
||||||
@@ -107,8 +105,6 @@ export function processCombatTick(
|
|||||||
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
||||||
rawMana = eAfterCost.rawMana;
|
rawMana = eAfterCost.rawMana;
|
||||||
elements = eAfterCost.elements;
|
elements = eAfterCost.elements;
|
||||||
totalManaGathered += eSpellDef.cost.amount;
|
|
||||||
|
|
||||||
// Calculate damage
|
// Calculate damage
|
||||||
const eFloorElement = getFloorElement(currentFloor);
|
const eFloorElement = getFloorElement(currentFloor);
|
||||||
const eDamage = calcDamage(
|
const eDamage = calcDamage(
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export const useCombatStore = create<CombatState>()(
|
|||||||
currentFloor: state.currentFloor,
|
currentFloor: state.currentFloor,
|
||||||
maxFloorReached: state.maxFloorReached,
|
maxFloorReached: state.maxFloorReached,
|
||||||
spells: state.spells,
|
spells: state.spells,
|
||||||
activeSpell: state.activeAction,
|
activeSpell: state.activeSpell,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,9 +80,10 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
let rawMana = mana.rawMana;
|
let rawMana = mana.rawMana;
|
||||||
const elements = { ...mana.elements };
|
const elements = { ...mana.elements };
|
||||||
let newXP = s.totalXP;
|
let newXP = s.totalXP;
|
||||||
|
const newDisciplines = { ...s.disciplines };
|
||||||
|
|
||||||
for (const id of s.activeIds) {
|
for (const id of s.activeIds) {
|
||||||
const disc = s.disciplines[id];
|
const disc = newDisciplines[id];
|
||||||
if (!disc) continue;
|
if (!disc) continue;
|
||||||
if (disc.paused) continue;
|
if (disc.paused) continue;
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
||||||
|
|
||||||
if (!available || available < drain) {
|
if (!available || available < drain) {
|
||||||
disc.paused = true;
|
newDisciplines[id] = { ...disc, paused: true };
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
elements[def.manaType].current -= drain;
|
elements[def.manaType].current -= drain;
|
||||||
}
|
}
|
||||||
|
|
||||||
disc.xp += 1;
|
newDisciplines[id] = { ...disc, xp: disc.xp + 1 };
|
||||||
newXP += 1;
|
newXP += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
set({
|
set({
|
||||||
disciplines: s.disciplines,
|
disciplines: newDisciplines,
|
||||||
totalXP: newXP,
|
totalXP: newXP,
|
||||||
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
|
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
|
|||||||
|
|
||||||
const startFloor = 1;
|
const startFloor = 1;
|
||||||
|
|
||||||
useUIStore.getState().resetUI();
|
useUIStore.getState().reset();
|
||||||
usePrestigeStore.getState().resetPrestige();
|
usePrestigeStore.getState().resetPrestige();
|
||||||
useManaStore.getState().resetMana({}, {}, {}, {});
|
useManaStore.getState().resetMana({}, {}, {}, {});
|
||||||
useCombatStore.getState().resetCombat(startFloor);
|
useCombatStore.getState().resetCombat(startFloor);
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
// Combat - delegate to combatStore
|
// Combat - delegate to combatStore
|
||||||
if (combatState.currentAction === 'climb') {
|
if (combatState.currentAction === 'climb') {
|
||||||
const combatResult = useCombatStore.getState().processCombatTick(
|
const combatResult = useCombatStore.getState().processCombatTick(
|
||||||
{},
|
|
||||||
rawMana,
|
rawMana,
|
||||||
elements,
|
elements,
|
||||||
maxMana,
|
maxMana,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface UIState {
|
|||||||
paused: boolean;
|
paused: boolean;
|
||||||
gameOver: boolean;
|
gameOver: boolean;
|
||||||
victory: boolean;
|
victory: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
addLog: (message: string) => void;
|
addLog: (message: string) => void;
|
||||||
clearLogs: () => void;
|
clearLogs: () => void;
|
||||||
@@ -21,7 +21,6 @@ export interface UIState {
|
|||||||
setPaused: (paused: boolean) => void;
|
setPaused: (paused: boolean) => void;
|
||||||
setGameOver: (gameOver: boolean, victory?: boolean) => void;
|
setGameOver: (gameOver: boolean, victory?: boolean) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
resetUI: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LOGS = 50;
|
const MAX_LOGS = 50;
|
||||||
@@ -31,29 +30,29 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
paused: false,
|
paused: false,
|
||||||
gameOver: false,
|
gameOver: false,
|
||||||
victory: false,
|
victory: false,
|
||||||
|
|
||||||
addLog: (message: string) => {
|
addLog: (message: string) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
logs: [message, ...state.logs.slice(0, MAX_LOGS - 1)],
|
logs: [message, ...state.logs.slice(0, MAX_LOGS - 1)],
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
clearLogs: () => {
|
clearLogs: () => {
|
||||||
set({ logs: [] });
|
set({ logs: [] });
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePause: () => {
|
togglePause: () => {
|
||||||
set((state) => ({ paused: !state.paused }));
|
set((state) => ({ paused: !state.paused }));
|
||||||
},
|
},
|
||||||
|
|
||||||
setPaused: (paused: boolean) => {
|
setPaused: (paused: boolean) => {
|
||||||
set({ paused });
|
set({ paused });
|
||||||
},
|
},
|
||||||
|
|
||||||
setGameOver: (gameOver: boolean, victory: boolean = false) => {
|
setGameOver: (gameOver: boolean, victory: boolean = false) => {
|
||||||
set({ gameOver, victory });
|
set({ gameOver, victory });
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
set({
|
set({
|
||||||
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
||||||
@@ -62,13 +61,4 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
victory: false,
|
victory: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetUI: () => {
|
|
||||||
set({
|
|
||||||
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
|
||||||
paused: false,
|
|
||||||
gameOver: false,
|
|
||||||
victory: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
|
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
|
||||||
|
|
||||||
|
export type ManaType = 'raw' | 'fire' | 'water' | 'air' | 'earth' | 'light' | 'dark' | 'death' | 'transference' | 'metal' | 'sand' | 'lightning' | 'crystal' | 'stellar' | 'void';
|
||||||
|
|
||||||
export interface ElementDef {
|
export interface ElementDef {
|
||||||
name: string;
|
name: string;
|
||||||
sym: string;
|
sym: string;
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export interface EnemyState {
|
|||||||
maxHP: number;
|
maxHP: number;
|
||||||
armor: number; // Damage reduction (0-1)
|
armor: number; // Damage reduction (0-1)
|
||||||
dodgeChance: number; // For speed rooms (0-1)
|
dodgeChance: number; // For speed rooms (0-1)
|
||||||
healthRegen?: number; // HP regenerated per tick (0-1 as percentage of max HP)
|
|
||||||
barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP)
|
barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP)
|
||||||
element: string;
|
element: string;
|
||||||
}
|
}
|
||||||
@@ -262,6 +261,24 @@ export interface GameState {
|
|||||||
|
|
||||||
// ─── Action Types for Store ─────────────────────────────────────────────
|
// ─── Action Types for Store ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PrestigeDef {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
max: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LootDrop {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
rarity: string;
|
||||||
|
type: string;
|
||||||
|
minFloor: number;
|
||||||
|
dropChance: number;
|
||||||
|
guardianOnly?: boolean;
|
||||||
|
amount?: { min: number; max: number };
|
||||||
|
}
|
||||||
|
|
||||||
export type GameActionType =
|
export type GameActionType =
|
||||||
| { type: 'TICK' }
|
| { type: 'TICK' }
|
||||||
| { type: 'GATHER_MANA' }
|
| { type: 'GATHER_MANA' }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Re-export all types from domain-specific files
|
// Re-export all types from domain-specific files
|
||||||
|
|
||||||
// Element types
|
// Element types
|
||||||
export type { ElementCategory, ElementDef, ElementState } from './elements';
|
export type { ElementCategory, ElementDef, ElementState, ManaType } from './elements';
|
||||||
|
|
||||||
// Attunement types
|
// Attunement types
|
||||||
export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements';
|
export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements';
|
||||||
@@ -46,4 +46,6 @@ export type {
|
|||||||
GameActionType,
|
GameActionType,
|
||||||
ActivityEventType,
|
ActivityEventType,
|
||||||
ActivityLogEntry,
|
ActivityLogEntry,
|
||||||
|
PrestigeDef,
|
||||||
|
LootDrop,
|
||||||
} from './game';
|
} from './game';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface SpellDef {
|
|||||||
isAoe?: boolean; // AOE spell that hits multiple enemies
|
isAoe?: boolean; // AOE spell that hits multiple enemies
|
||||||
aoeTargets?: number; // Number of enemies hit by AOE
|
aoeTargets?: number; // Number of enemies hit by AOE
|
||||||
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
|
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
|
||||||
|
grimoire?: boolean; // Whether this spell appears in the grimoire
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpellEffect {
|
export interface SpellEffect {
|
||||||
|
|||||||
@@ -61,20 +61,6 @@ export function getDodgeChance(floor: number): number {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get health regen for an enemy (0-1 as percentage of max HP per tick)
|
|
||||||
export function getEnemyHealthRegen(floor: number, element: string): number {
|
|
||||||
// Higher floors have a chance for enemies with health regen
|
|
||||||
if (floor < 15) return 0;
|
|
||||||
|
|
||||||
// Health regen becomes more common on higher floors
|
|
||||||
const regenChance = Math.min(0.3, (floor - 15) * 0.005); // Max 30% chance
|
|
||||||
if (Math.random() > regenChance) return 0;
|
|
||||||
|
|
||||||
// Scale regen with floor (0.5% to 3% of max HP per tick)
|
|
||||||
const floorProgress = Math.min(1, (floor - 15) / 85);
|
|
||||||
return 0.005 + floorProgress * 0.025;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get barrier for an enemy (0-1 as percentage of max HP)
|
// Get barrier for an enemy (0-1 as percentage of max HP)
|
||||||
export function getEnemyBarrier(floor: number, element: string): number {
|
export function getEnemyBarrier(floor: number, element: string): number {
|
||||||
// Barrier appears on higher floors, more common with certain elements
|
// Barrier appears on higher floors, more common with certain elements
|
||||||
@@ -110,7 +96,6 @@ export function generateSwarmEnemies(floor: number): EnemyState[] {
|
|||||||
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
||||||
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
|
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
|
||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
healthRegen: getEnemyHealthRegen(floor, element),
|
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
});
|
});
|
||||||
@@ -136,7 +121,6 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
maxHP: guardian.hp,
|
maxHP: guardian.hp,
|
||||||
armor: guardian.armor || 0,
|
armor: guardian.armor || 0,
|
||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
healthRegen: 0.01, // Guardians have 1% HP regen per tick
|
|
||||||
barrier: 0,
|
barrier: 0,
|
||||||
element: guardian.element,
|
element: guardian.element,
|
||||||
}],
|
}],
|
||||||
@@ -159,7 +143,6 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
maxHP: baseHP,
|
maxHP: baseHP,
|
||||||
armor: getFloorArmor(floor),
|
armor: getFloorArmor(floor),
|
||||||
dodgeChance: getDodgeChance(floor),
|
dodgeChance: getDodgeChance(floor),
|
||||||
healthRegen: getEnemyHealthRegen(floor, element),
|
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
}],
|
}],
|
||||||
@@ -192,7 +175,6 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
maxHP: baseHP,
|
maxHP: baseHP,
|
||||||
armor: getFloorArmor(floor),
|
armor: getFloorArmor(floor),
|
||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
healthRegen: getEnemyHealthRegen(floor, element),
|
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
}],
|
}],
|
||||||
|
|||||||
Reference in New Issue
Block a user