Initial commit

This commit is contained in:
Z User
2026-04-03 17:23:15 +00:00
commit 4f474dbcf3
192 changed files with 47527 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
'use client';
import { useGameContext } from '../GameContext';
export function GameFooter() {
const { store } = useGameContext();
return (
<footer className="sticky bottom-0 bg-gray-900/80 border-t border-gray-700 px-4 py-2 text-center text-xs text-gray-500">
<span className="text-gray-400">Loop {store.loopCount + 1}</span>
{' • '}
<span>Pacts: {store.signedPacts.length}/{store.pactSlots}</span>
{' • '}
<span>Spells: {Object.values(store.spells).filter((s) => s.learned).length}</span>
{' • '}
<span>Skills: {Object.values(store.skills).reduce((a, b) => a + b, 0)}</span>
</footer>
);
}
+79
View File
@@ -0,0 +1,79 @@
'use client';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Pause, Play } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { formatTime } from '../types';
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
import { fmt } from '@/lib/game/stores';
export function GameHeader() {
const { store } = useGameContext();
// Calendar rendering
const renderCalendar = () => {
const days: React.ReactElement[] = [];
for (let d = 1; d <= MAX_DAY; d++) {
let dayClass = 'w-7 h-7 rounded text-xs flex items-center justify-center font-mono border transition-all ';
if (d < store.day) {
dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400';
} else if (d === store.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 days;
};
return (
<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">
<div className="text-center">
<div className="text-lg font-bold game-mono text-amber-400">Day {store.day}</div>
<div className="text-xs text-gray-400">{formatTime(store.hour)}</div>
</div>
<div className="text-center">
<div className="text-lg font-bold game-mono text-purple-400">{fmt(store.insight)}</div>
<div className="text-xs text-gray-400">Insight</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => store.togglePause()}
className="text-gray-400 hover:text-white"
>
{store.paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
</Button>
</div>
</div>
{/* Calendar */}
<div className="mt-2 flex gap-1 overflow-x-auto pb-1">{renderCalendar()}</div>
</header>
);
}
+141
View File
@@ -0,0 +1,141 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Card, CardContent } from '@/components/ui/card';
import { Zap, Sparkles, Swords, BookOpen, FlaskConical, type LucideIcon } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { fmt, fmtDec } from '@/lib/game/stores';
import type { GameAction } from '@/lib/game/types';
export function GameSidebar() {
const {
store,
maxMana,
clickMana,
effectiveRegen,
meditationMultiplier,
floorElemDef,
} = useGameContext();
const [isGathering, setIsGathering] = useState(false);
// 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]);
const handleGatherStart = useCallback(() => {
setIsGathering(true);
store.gatherMana();
}, [store]);
const handleGatherEnd = useCallback(() => {
setIsGathering(false);
}, []);
// Action buttons
const actions: { id: GameAction; label: string; icon: LucideIcon }[] = [
{ id: 'meditate', label: 'Meditate', icon: Sparkles },
{ id: 'climb', label: 'Climb', icon: Swords },
{ id: 'study', label: 'Study', icon: BookOpen },
{ id: 'convert', label: 'Convert', icon: FlaskConical },
];
return (
<aside className="w-full md:w-64 bg-gray-900/50 border-b md:border-b-0 md:border-r border-gray-700 p-4 space-y-4">
{/* Mana Panel */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 space-y-3">
<div>
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold game-mono text-blue-400">{fmt(store.rawMana)}</span>
<span className="text-sm text-gray-400">/ {fmt(maxMana)}</span>
</div>
<div className="text-xs text-gray-400">
+{fmtDec(effectiveRegen)} mana/hr{' '}
{meditationMultiplier > 1.01 && (
<span className="text-purple-400">({fmtDec(meditationMultiplier, 1)}x med)</span>
)}
</div>
</div>
<Progress value={(store.rawMana / maxMana) * 100} className="h-2 bg-gray-800" />
<Button
className={`w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 ${
isGathering ? 'animate-pulse' : ''
}`}
onMouseDown={handleGatherStart}
onMouseUp={handleGatherEnd}
onMouseLeave={handleGatherEnd}
onTouchStart={handleGatherStart}
onTouchEnd={handleGatherEnd}
>
<Zap className="w-4 h-4 mr-2" />
Gather +{clickMana} Mana
{isGathering && <span className="ml-2 text-xs">(Holding...)</span>}
</Button>
</CardContent>
</Card>
{/* Actions */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4">
<div className="text-xs text-amber-400 game-panel-title mb-2">Current Action</div>
<div className="grid grid-cols-2 gap-2">
{actions.map(({ id, label, icon: Icon }) => (
<Button
key={id}
variant={store.currentAction === id ? 'default' : 'outline'}
size="sm"
className={`h-9 ${
store.currentAction === id
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'
}`}
onClick={() => store.setAction(id)}
>
<Icon className="w-4 h-4 mr-1" />
{label}
</Button>
))}
</div>
</CardContent>
</Card>
{/* Floor Status */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 space-y-2">
<div className="text-xs text-amber-400 game-panel-title mb-2">Floor Status</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Current</span>
<span className="text-lg font-bold" style={{ color: floorElemDef?.color }}>
{store.currentFloor}
</span>
</div>
<Progress value={(store.floorHP / store.floorMaxHP) * 100} className="h-2 bg-gray-800" />
<div className="text-xs text-gray-400 game-mono">
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP
</div>
</CardContent>
</Card>
</aside>
);
}