From e8b8fc26c7732568067c49d24e12969f4d9f3c40 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Mon, 11 May 2026 14:23:39 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20TASK-006=20left=20panel=20redesign=20?= =?UTF-8?q?=E2=80=94=205-section=20layout=20with=20attunement=20status=20a?= =?UTF-8?q?nd=20activity=20log,=20remove=20CalendarDisplay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/project-structure.txt | 2 + src/app/components/LeftPanel.tsx | 107 +++++++++-------------- src/components/game/ActivityLogPanel.tsx | 19 ++++ src/components/game/AttunementStatus.tsx | 103 ++++++++++++++++++++++ src/components/game/index.ts | 2 + src/components/game/tabs/ActivityLog.tsx | 5 +- 6 files changed, 172 insertions(+), 66 deletions(-) create mode 100644 src/components/game/ActivityLogPanel.tsx create mode 100644 src/components/game/AttunementStatus.tsx diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 73af7d9..bd78f51 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -182,6 +182,8 @@ Mana-Loop/ │ │ │ │ └── index.ts │ │ │ ├── AchievementsDisplay.tsx │ │ │ ├── ActionButtons.tsx +│ │ │ ├── ActivityLogPanel.tsx +│ │ │ ├── AttunementStatus.tsx │ │ │ ├── CalendarDisplay.tsx │ │ │ ├── ConfirmDialog.tsx │ │ │ ├── CraftingProgress.tsx diff --git a/src/app/components/LeftPanel.tsx b/src/app/components/LeftPanel.tsx index b47e0c1..278f98c 100644 --- a/src/app/components/LeftPanel.tsx +++ b/src/app/components/LeftPanel.tsx @@ -3,9 +3,11 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Mountain } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; import { ManaDisplay } from '@/components/game'; import { ActionButtons } from '@/components/game'; -import { CalendarDisplay } from '@/components/game'; +import { AttunementStatus } from '@/components/game/AttunementStatus'; +import { ActivityLogPanel } from '@/components/game/ActivityLogPanel'; import { DebugName } from '@/lib/game/debug-context'; import { useGameStore, useManaStore, useSkillStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores'; import { getUnifiedEffects } from '@/lib/game/effects'; @@ -15,52 +17,34 @@ import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@ export function LeftPanel() { const [isGathering, setIsGathering] = useState(false); - // Get state from modular stores const rawMana = useManaStore((s) => s.rawMana); const elements = useManaStore((s) => s.elements); const meditateTicks = useManaStore((s) => s.meditateTicks); - const skills = useSkillStore((s) => s.skills); const skillTiers = useSkillStore((s) => s.skillTiers); const skillUpgrades = useSkillStore((s) => s.skillUpgrades); - const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); - const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); - const gatherMana = useGameStore((s) => s.gatherMana); - const day = useGameStore((s) => s.day); - const hour = useGameStore((s) => s.hour); - const spireMode = useCombatStore((s) => s.spireMode); const enterSpireMode = useCombatStore((s) => s.enterSpireMode); const currentAction = useCombatStore((s) => s.currentAction); - const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget); - const designProgress = useCraftingStore((s) => s.designProgress); const designProgress2 = useCraftingStore((s) => s.designProgress2); const preparationProgress = useCraftingStore((s) => s.preparationProgress); const applicationProgress = useCraftingStore((s) => s.applicationProgress); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress); - const handleGatherStart = () => { - setIsGathering(true); - gatherMana(); - }; - - const handleGatherEnd = () => { - setIsGathering(false); - }; + const handleGatherStart = () => { setIsGathering(true); gatherMana(); }; + const handleGatherEnd = () => { setIsGathering(false); }; useEffect(() => { if (!isGathering) return; - let lastGatherTime = 0; const minGatherInterval = 100; let animationFrameId: number; - const gatherLoop = (timestamp: number) => { if (timestamp - lastGatherTime >= minGatherInterval) { gatherMana(); @@ -68,36 +52,21 @@ export function LeftPanel() { } animationFrameId = requestAnimationFrame(gatherLoop); }; - animationFrameId = requestAnimationFrame(gatherLoop); return () => cancelAnimationFrame(animationFrameId); }, [isGathering, gatherMana]); - const upgradeEffects = getUnifiedEffects({ - skillUpgrades, - skillTiers, - equippedInstances, - equipmentInstances, - }); - - const maxMana = computeTotalMaxMana( - { skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, - upgradeEffects - ); - const baseRegen = computeTotalRegen( - { skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, - upgradeEffects - ); - const clickMana = computeTotalClickMana( - { skills, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, - upgradeEffects - ); + const upgradeEffects = getUnifiedEffects({ skillUpgrades, skillTiers, equippedInstances, equipmentInstances }); + const maxMana = computeTotalMaxMana({ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); + const baseRegen = computeTotalRegen({ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); + const clickMana = computeTotalClickMana({ skills, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency); - const incursionStrength = getIncursionStrength(day, hour); + const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour)); const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; return ( -
+
+ {/* 1. Mana Display */} + {/* 2. Spire Entry */} {!spireMode && ( - )} + {/* 3. Current Action */} {!spireMode && ( - + + + + + )} - - + {/* 4. Attunement Status */} + {!spireMode && ( + + + + + + + + )} + + {/* 5. Activity Log */} + +
); -} +} \ No newline at end of file diff --git a/src/components/game/ActivityLogPanel.tsx b/src/components/game/ActivityLogPanel.tsx new file mode 100644 index 0000000..ff74de3 --- /dev/null +++ b/src/components/game/ActivityLogPanel.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { useCombatStore } from '@/lib/game/stores'; +import { ActivityLog } from './tabs/ActivityLog'; + +/** + * Activity log panel for the left sidebar. + * Wraps the existing ActivityLog tab component with store integration, + * showing only the most recent 20 entries. + */ +export function ActivityLogPanel() { + const activityLog = useCombatStore((s) => s.activityLog); + + return ( + + ); +} + +ActivityLogPanel.displayName = 'ActivityLogPanel'; \ No newline at end of file diff --git a/src/components/game/AttunementStatus.tsx b/src/components/game/AttunementStatus.tsx new file mode 100644 index 0000000..327654b --- /dev/null +++ b/src/components/game/AttunementStatus.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useAttunementStore } from '@/lib/game/stores'; +import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements'; +import { Separator } from '@/components/ui/separator'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +const SLOT_LABELS: Record = { + rightHand: 'R. Hand', + leftHand: 'L. Hand', + head: 'Head', + back: 'Back', + chest: 'Chest', + leftLeg: 'L. Leg', + rightLeg: 'R. Leg', +}; + +export function AttunementStatus() { + const attunements = useAttunementStore((s) => s.attunements); + + const activeAttunements = Object.entries(attunements) + .filter(([, state]) => state.active) + .sort(([, a], [, b]) => { + const orderA = Object.values(ATTUNEMENTS_DEF).findIndex(d => d.id === a.id); + const orderB = Object.values(ATTUNEMENTS_DEF).findIndex(d => d.id === b.id); + return orderA - orderB; + }); + + const xpForNext = (level: number) => { + if (level <= 1) return 0; + if (level === 2) return 1000; + return Math.floor(1000 * Math.pow(2, level - 2) * (level >= 3 ? 1.25 : 1)); + }; + + return ( +
+
+ Attunements + {activeAttunements.length} active +
+ +
+ {activeAttunements.length === 0 ? ( +
No attunements active
+ ) : ( + activeAttunements.map(([id, state]) => { + const def = ATTUNEMENTS_DEF[id]; + if (!def) return null; + const nextXp = xpForNext(state.level); + const xpProgress = nextXp > 0 ? (state.experience / nextXp) * 100 : 0; + + return ( + + + +
+ {def.icon} +
+
+ + {def.name} + + + Lv.{state.level} + +
+
+ {SLOT_LABELS[def.slot] || def.slot} + {nextXp > 0 && ( + + {Math.floor(state.experience).toLocaleString()}/{nextXp.toLocaleString()} XP + + )} +
+ {nextXp > 0 && ( +
+
+
+ )} +
+
+ + +

{def.desc}

+
+ + + ); + }) + )} +
+
+ ); +} + +AttunementStatus.displayName = 'AttunementStatus'; \ No newline at end of file diff --git a/src/components/game/index.ts b/src/components/game/index.ts index dafd651..212d241 100755 --- a/src/components/game/index.ts +++ b/src/components/game/index.ts @@ -16,3 +16,5 @@ export { StudyProgress } from './StudyProgress'; export { ManaDisplay } from './ManaDisplay'; export { TimeDisplay } from './TimeDisplay'; export { UpgradeDialog } from './UpgradeDialog'; +export { AttunementStatus } from './AttunementStatus'; +export { ActivityLogPanel } from './ActivityLogPanel'; diff --git a/src/components/game/tabs/ActivityLog.tsx b/src/components/game/tabs/ActivityLog.tsx index 464e8c8..d1f7970 100644 --- a/src/components/game/tabs/ActivityLog.tsx +++ b/src/components/game/tabs/ActivityLog.tsx @@ -6,9 +6,10 @@ import type { ActivityLogEntry } from '@/lib/game/types'; interface ActivityLogProps { activityLog?: ActivityLogEntry[]; + maxEntries?: number; } -export function ActivityLog({ activityLog }: ActivityLogProps) { +export function ActivityLog({ activityLog, maxEntries = 50 }: ActivityLogProps) { const entries = activityLog || []; return ( @@ -19,7 +20,7 @@ export function ActivityLog({ activityLog }: ActivityLogProps) {
- {entries.slice(0, 50).map((entry, i) => { + {entries.slice(0, maxEntries).map((entry, i) => { const isLatest = i === 0; const color = getEventStyle(entry.eventType); return (