Initial commit
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
This commit is contained in:
5
src/app/api/route.ts
Executable file
5
src/app/api/route.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello, world!" });
|
||||
}
|
||||
300
src/app/globals.css
Executable file
300
src/app/globals.css
Executable file
@@ -0,0 +1,300 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: #060811;
|
||||
--foreground: #c8d8f8;
|
||||
--card: #0C1020;
|
||||
--card-foreground: #c8d8f8;
|
||||
--popover: #111628;
|
||||
--popover-foreground: #c8d8f8;
|
||||
--primary: #3B6FE8;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #1e2a45;
|
||||
--secondary-foreground: #c8d8f8;
|
||||
--muted: #181f35;
|
||||
--muted-foreground: #7a92c0;
|
||||
--accent: #2a3a60;
|
||||
--accent-foreground: #c8d8f8;
|
||||
--destructive: #C0392B;
|
||||
--border: #1e2a45;
|
||||
--input: #1e2a45;
|
||||
--ring: #3B6FE8;
|
||||
--chart-1: #FF6B35;
|
||||
--chart-2: #4ECDC4;
|
||||
--chart-3: #9B59B6;
|
||||
--chart-4: #2ECC71;
|
||||
--chart-5: #FFD700;
|
||||
--sidebar: #0C1020;
|
||||
--sidebar-foreground: #c8d8f8;
|
||||
--sidebar-primary: #D4A843;
|
||||
--sidebar-primary-foreground: #0C1020;
|
||||
--sidebar-accent: #1e2a45;
|
||||
--sidebar-accent-foreground: #c8d8f8;
|
||||
--sidebar-border: #1e2a45;
|
||||
--sidebar-ring: #D4A843;
|
||||
|
||||
/* Game-specific colors */
|
||||
--game-bg: #060811;
|
||||
--game-bg1: #0C1020;
|
||||
--game-bg2: #111628;
|
||||
--game-bg3: #181f35;
|
||||
--game-border: #1e2a45;
|
||||
--game-border2: #2a3a60;
|
||||
--game-text: #c8d8f8;
|
||||
--game-text2: #7a92c0;
|
||||
--game-text3: #4a5f8a;
|
||||
--game-gold: #D4A843;
|
||||
--game-gold2: #A87830;
|
||||
--game-purple: #7C5CBF;
|
||||
--game-purpleL: #A07EE0;
|
||||
--game-accent: #3B6FE8;
|
||||
--game-accentL: #5B8FFF;
|
||||
--game-danger: #C0392B;
|
||||
--game-success: #27AE60;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #060811;
|
||||
--foreground: #c8d8f8;
|
||||
--card: #0C1020;
|
||||
--card-foreground: #c8d8f8;
|
||||
--popover: #111628;
|
||||
--popover-foreground: #c8d8f8;
|
||||
--primary: #5B8FFF;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #1e2a45;
|
||||
--secondary-foreground: #c8d8f8;
|
||||
--muted: #181f35;
|
||||
--muted-foreground: #7a92c0;
|
||||
--accent: #2a3a60;
|
||||
--accent-foreground: #c8d8f8;
|
||||
--destructive: #C0392B;
|
||||
--border: #1e2a45;
|
||||
--input: #1e2a45;
|
||||
--ring: #5B8FFF;
|
||||
--chart-1: #FF6B35;
|
||||
--chart-2: #4ECDC4;
|
||||
--chart-3: #9B59B6;
|
||||
--chart-4: #2ECC71;
|
||||
--chart-5: #FFD700;
|
||||
--sidebar: #0C1020;
|
||||
--sidebar-foreground: #c8d8f8;
|
||||
--sidebar-primary: #D4A843;
|
||||
--sidebar-primary-foreground: #0C1020;
|
||||
--sidebar-accent: #1e2a45;
|
||||
--sidebar-accent-foreground: #c8d8f8;
|
||||
--sidebar-border: #1e2a45;
|
||||
--sidebar-ring: #D4A843;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'Crimson Text', Georgia, serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* Game-specific styles */
|
||||
.game-root {
|
||||
font-family: 'Crimson Text', Georgia, serif;
|
||||
background: var(--game-bg);
|
||||
color: var(--game-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 10%, #0D1535 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 90%, #0A0A20 0%, transparent 50%),
|
||||
repeating-linear-gradient(0deg, transparent, transparent 40px, rgba(30,42,69,0.15) 40px, rgba(30,42,69,0.15) 41px),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 40px, rgba(30,42,69,0.15) 40px, rgba(30,42,69,0.15) 41px);
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
background: linear-gradient(135deg, var(--game-gold) 0%, var(--game-purpleL) 50%, var(--game-accentL) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.game-panel-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--game-border2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--game-purple);
|
||||
}
|
||||
|
||||
/* Mana bar animation */
|
||||
@keyframes mana-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.mana-bar-animated {
|
||||
animation: mana-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Glow effects */
|
||||
.glow-gold {
|
||||
box-shadow: 0 0 15px rgba(212, 168, 67, 0.4);
|
||||
}
|
||||
|
||||
.glow-purple {
|
||||
box-shadow: 0 0 15px rgba(124, 92, 191, 0.4);
|
||||
}
|
||||
|
||||
.glow-accent {
|
||||
box-shadow: 0 0 15px rgba(60, 111, 232, 0.4);
|
||||
}
|
||||
|
||||
/* Button hover effects */
|
||||
.btn-game {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-game:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Card hover effects */
|
||||
.card-game {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card-game:hover {
|
||||
border-color: var(--game-border2);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Element pill styles */
|
||||
.elem-pill {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.elem-pill:hover {
|
||||
filter: brightness(1.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Calendar day styles */
|
||||
.day-cell {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.day-current {
|
||||
box-shadow: 0 0 8px rgba(60, 111, 232, 0.5);
|
||||
}
|
||||
|
||||
.day-incursion {
|
||||
border-color: rgba(192, 57, 43, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Game over overlay */
|
||||
.game-overlay {
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(6, 8, 17, 0.92);
|
||||
}
|
||||
|
||||
/* Pact badge */
|
||||
.pact-badge {
|
||||
box-shadow: 0 0 10px rgba(212, 168, 67, 0.3);
|
||||
}
|
||||
|
||||
/* Spell card active */
|
||||
.spell-active {
|
||||
border-color: var(--game-gold);
|
||||
background: rgba(212, 168, 67, 0.1);
|
||||
}
|
||||
|
||||
/* Tab active */
|
||||
.tab-active {
|
||||
color: var(--game-gold);
|
||||
border-bottom-color: var(--game-gold);
|
||||
}
|
||||
|
||||
/* Skill dot filled */
|
||||
.skill-dot-filled {
|
||||
background: var(--game-purpleL);
|
||||
box-shadow: 0 0 6px rgba(160, 126, 224, 0.5);
|
||||
}
|
||||
|
||||
/* Log entry */
|
||||
.log-entry-new {
|
||||
color: var(--game-text);
|
||||
}
|
||||
|
||||
.log-entry-old {
|
||||
color: var(--game-text2);
|
||||
}
|
||||
53
src/app/layout.tsx
Executable file
53
src/app/layout.tsx
Executable file
@@ -0,0 +1,53 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Z.ai Code Scaffold - AI-Powered Development",
|
||||
description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.",
|
||||
keywords: ["Z.ai", "Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "AI development", "React"],
|
||||
authors: [{ name: "Z.ai Team" }],
|
||||
icons: {
|
||||
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Z.ai Code Scaffold",
|
||||
description: "AI-powered development with modern React stack",
|
||||
url: "https://chat.z.ai",
|
||||
siteName: "Z.ai",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Z.ai Code Scaffold",
|
||||
description: "AI-powered development with modern React stack",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
439
src/app/page.tsx
Executable file
439
src/app/page.tsx
Executable file
@@ -0,0 +1,439 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
|
||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||
import { getDamageBreakdown } from '@/lib/game/computed-stats';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { formatHour } from '@/lib/game/formatting';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab, DebugTab } from '@/components/game/tabs';
|
||||
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
|
||||
export default function ManaLoopGame() {
|
||||
const [activeTab, setActiveTab] = useState('spire');
|
||||
const [isGathering, setIsGathering] = useState(false);
|
||||
|
||||
// Game store
|
||||
const store = useGameStore();
|
||||
const gameLoop = useGameLoop();
|
||||
|
||||
// Computed effects from upgrades and equipment
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
|
||||
// Derived stats
|
||||
const maxMana = computeMaxMana(store, upgradeEffects);
|
||||
const baseRegen = computeRegen(store, upgradeEffects);
|
||||
const clickMana = computeClickMana(store);
|
||||
const floorElem = getFloorElement(store.currentFloor);
|
||||
const floorElemDef = ELEMENTS[floorElem];
|
||||
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
||||
const currentGuardian = GUARDIANS[store.currentFloor];
|
||||
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency);
|
||||
const incursionStrength = getIncursionStrength(store.day, store.hour);
|
||||
const studySpeedMult = getStudySpeedMultiplier(store.skills);
|
||||
const studyCostMult = getStudyCostMultiplier(store.skills);
|
||||
|
||||
// Effective regen with incursion penalty
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
// Mana Cascade bonus
|
||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
// Effective regen
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier;
|
||||
|
||||
// Get all active spells from equipment
|
||||
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
||||
|
||||
// Compute total DPS
|
||||
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
|
||||
|
||||
// Auto-gather while holding
|
||||
useEffect(() => {
|
||||
if (!isGathering) return;
|
||||
|
||||
let lastGatherTime = 0;
|
||||
const minGatherInterval = 100;
|
||||
let animationFrameId: number;
|
||||
|
||||
const gatherLoop = (timestamp: number) => {
|
||||
if (timestamp - lastGatherTime >= minGatherInterval) {
|
||||
store.gatherMana();
|
||||
lastGatherTime = timestamp;
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(gatherLoop);
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(gatherLoop);
|
||||
return () => cancelAnimationFrame(animationFrameId);
|
||||
}, [isGathering, store]);
|
||||
|
||||
// Handle gather button events
|
||||
const handleGatherStart = () => {
|
||||
setIsGathering(true);
|
||||
store.gatherMana();
|
||||
};
|
||||
|
||||
const handleGatherEnd = () => {
|
||||
setIsGathering(false);
|
||||
};
|
||||
|
||||
// Start game loop
|
||||
useEffect(() => {
|
||||
const cleanup = gameLoop.start();
|
||||
return cleanup;
|
||||
}, [gameLoop]);
|
||||
|
||||
// Check if spell can be cast
|
||||
const canCastSpell = (spellId: string): boolean => {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell) return false;
|
||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
||||
};
|
||||
|
||||
// Game Over Screen
|
||||
if (store.gameOver) {
|
||||
return (
|
||||
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
||||
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
|
||||
{store.victory ? 'VICTORY!' : 'LOOP ENDS'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-center text-gray-400">
|
||||
{store.victory
|
||||
? 'The Awakened One falls! Your power echoes through eternity.'
|
||||
: 'The time loop resets... but you remember.'}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-gray-800 rounded">
|
||||
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(store.loopInsight)}</div>
|
||||
<div className="text-xs text-gray-400">Insight Gained</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800 rounded">
|
||||
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
|
||||
<div className="text-xs text-gray-400">Best Floor</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800 rounded">
|
||||
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
|
||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800 rounded">
|
||||
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
|
||||
<div className="text-xs text-gray-400">Total Loops</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||
size="lg"
|
||||
onClick={() => store.startNewLoop()}
|
||||
>
|
||||
Begin New Loop
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="game-root min-h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<TimeDisplay
|
||||
day={store.day}
|
||||
hour={store.hour}
|
||||
isPaused={store.isPaused}
|
||||
togglePause={store.togglePause}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
||||
{/* Left Panel - Mana & Actions */}
|
||||
<div className="md:w-80 space-y-4 flex-shrink-0">
|
||||
{/* Mana Display */}
|
||||
<ManaDisplay
|
||||
rawMana={store.rawMana}
|
||||
maxMana={maxMana}
|
||||
effectiveRegen={effectiveRegen}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
clickMana={clickMana}
|
||||
isGathering={isGathering}
|
||||
onGatherStart={handleGatherStart}
|
||||
onGatherEnd={handleGatherEnd}
|
||||
elements={store.elements}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<ActionButtons
|
||||
currentAction={store.currentAction}
|
||||
designProgress={store.designProgress}
|
||||
preparationProgress={store.preparationProgress}
|
||||
applicationProgress={store.applicationProgress}
|
||||
setAction={store.setAction}
|
||||
/>
|
||||
|
||||
{/* Calendar */}
|
||||
<CalendarDisplay
|
||||
day={store.day}
|
||||
hour={store.hour}
|
||||
incursionStrength={incursionStrength}
|
||||
/>
|
||||
|
||||
{/* Loot Inventory */}
|
||||
<LootInventoryDisplay
|
||||
inventory={store.lootInventory}
|
||||
elements={store.elements}
|
||||
equipmentInstances={store.equipmentInstances}
|
||||
onDeleteMaterial={store.deleteMaterial}
|
||||
onDeleteEquipment={store.deleteEquipmentInstance}
|
||||
/>
|
||||
|
||||
{/* Achievements */}
|
||||
<AchievementsDisplay
|
||||
achievements={store.achievements}
|
||||
gameState={{
|
||||
maxFloorReached: store.maxFloorReached,
|
||||
totalManaGathered: store.totalManaGathered,
|
||||
signedPacts: store.signedPacts,
|
||||
totalSpellsCast: store.totalSpellsCast,
|
||||
totalDamageDealt: store.totalDamageDealt,
|
||||
totalCraftsCompleted: store.totalCraftsCompleted,
|
||||
combo: store.combo,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Tabs */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
||||
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||||
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🔧 Debug</TabsTrigger>
|
||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="spire">
|
||||
<SpireTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attunements">
|
||||
<AttunementsTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="skills">
|
||||
<SkillsTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="spells">
|
||||
<SpellsTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="equipment">
|
||||
<EquipmentTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="crafting">
|
||||
<CraftingTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lab">
|
||||
<LabTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats">
|
||||
<StatsTab
|
||||
store={store}
|
||||
upgradeEffects={upgradeEffects}
|
||||
maxMana={maxMana}
|
||||
baseRegen={baseRegen}
|
||||
clickMana={clickMana}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
effectiveRegen={effectiveRegen}
|
||||
incursionStrength={incursionStrength}
|
||||
manaCascadeBonus={manaCascadeBonus}
|
||||
studySpeedMult={studySpeedMult}
|
||||
studyCostMult={studyCostMult}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="grimoire">
|
||||
{renderGrimoireTab()}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="debug">
|
||||
<DebugTab store={store} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
// Grimoire Tab (Prestige)
|
||||
function renderGrimoireTab() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Current Status */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||||
<div className="text-xs text-gray-400">Current Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||||
<div className="text-xs text-gray-400">Total Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
|
||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Signed Pacts */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Signed Pacts</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{store.signedPacts.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{store.signedPacts.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className="flex items-center justify-between p-2 rounded border"
|
||||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
||||
</div>
|
||||
<Badge className="bg-amber-900/50 text-amber-300">
|
||||
{guardian.pact}x multiplier
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Prestige Upgrades */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
|
||||
const level = store.prestigeUpgrades[id] || 0;
|
||||
const maxed = level >= def.max;
|
||||
const canBuy = !maxed && store.insight >= def.cost;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="p-3 rounded border border-gray-700 bg-gray-800/50"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{level}/{def.max}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canBuy ? 'default' : 'outline'}
|
||||
className="w-full"
|
||||
disabled={!canBuy}
|
||||
onClick={() => store.doPrestige(id)}
|
||||
>
|
||||
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Reset Game Button */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400">Reset All Progress</div>
|
||||
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
|
||||
store.resetGame();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Import TooltipProvider
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
Reference in New Issue
Block a user