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
+76
View File
@@ -0,0 +1,76 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { useGameContext } from '../GameContext';
import { fmt } from '@/lib/game/stores';
import { MemorySlotPicker } from './MemorySlotPicker';
export function GameOverScreen() {
const { store } = useGameContext();
const [memoriesConfirmed, setMemoriesConfirmed] = useState(false);
if (!store.gameOver) return null;
const handleStartNewLoop = () => {
store.startNewLoop();
};
return (
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50 overflow-auto py-4">
<div className="max-w-lg w-full mx-4 space-y-4">
<Card className="bg-gray-900 border-gray-600 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>
</CardContent>
</Card>
{/* Memory Slot Picker */}
{store.memorySlots > 0 && !memoriesConfirmed && (
<MemorySlotPicker onConfirm={() => setMemoriesConfirmed(true)} />
)}
<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={handleStartNewLoop}
disabled={store.memorySlots > 0 && !memoriesConfirmed}
>
Begin New Loop
{store.memorySlots > 0 && !memoriesConfirmed && ' (Confirm Memories First)'}
</Button>
</div>
</div>
);
}
+206
View File
@@ -0,0 +1,206 @@
'use client';
import { useState, useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Save, Trash2, Star, ChevronUp } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { SKILLS_DEF } from '@/lib/game/constants';
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
import type { Memory } from '@/lib/game/types';
interface MemorySlotPickerProps {
onConfirm?: () => void;
}
export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
const { store } = useGameContext();
const [selectedSkills, setSelectedSkills] = useState<Memory[]>(store.memories || []);
// Get all skills that have progress and can be saved
const saveableSkills = useMemo(() => {
const skills: { skillId: string; level: number; tier: number; upgrades: string[]; name: string }[] = [];
for (const [skillId, level] of Object.entries(store.skills)) {
if (level && level > 0) {
const baseSkillId = getBaseSkillId(skillId);
const tier = store.skillTiers?.[baseSkillId] || 1;
const tieredSkillId = tier > 1 ? `${baseSkillId}_t${tier}` : baseSkillId;
const upgrades = store.skillUpgrades?.[tieredSkillId] || [];
const skillDef = SKILLS_DEF[baseSkillId];
// Only include if it's a base skill (not a tiered variant in the skills object)
if (skillId === baseSkillId || skillId.includes('_t')) {
// Get the actual skill ID and level
const actualLevel = store.skills[tieredSkillId] || store.skills[baseSkillId] || 0;
if (actualLevel > 0) {
skills.push({
skillId: baseSkillId,
level: actualLevel,
tier,
upgrades,
name: skillDef?.name || baseSkillId,
});
}
}
}
}
// Remove duplicates and keep highest tier/level
const uniqueSkills = new Map<string, typeof skills[0]>();
for (const skill of skills) {
const existing = uniqueSkills.get(skill.skillId);
if (!existing || skill.tier > existing.tier || (skill.tier === existing.tier && skill.level > existing.level)) {
uniqueSkills.set(skill.skillId, skill);
}
}
return Array.from(uniqueSkills.values()).sort((a, b) => {
// Sort by tier then level then name
if (a.tier !== b.tier) return b.tier - a.tier;
if (a.level !== b.level) return b.level - a.level;
return a.name.localeCompare(b.name);
});
}, [store.skills, store.skillTiers, store.skillUpgrades]);
const isSkillSelected = (skillId: string) => selectedSkills.some(m => m.skillId === skillId);
const canAddMore = selectedSkills.length < store.memorySlots;
const toggleSkill = (skillId: string) => {
const existingIndex = selectedSkills.findIndex(m => m.skillId === skillId);
if (existingIndex >= 0) {
// Remove it
setSelectedSkills(selectedSkills.filter((_, i) => i !== existingIndex));
} else if (canAddMore) {
// Add it
const skill = saveableSkills.find(s => s.skillId === skillId);
if (skill) {
setSelectedSkills([...selectedSkills, {
skillId: skill.skillId,
level: skill.level,
tier: skill.tier,
upgrades: skill.upgrades,
}]);
}
}
};
const handleConfirm = () => {
// Clear and re-add selected memories
store.clearMemories();
for (const memory of selectedSkills) {
store.addMemory(memory);
}
onConfirm?.();
};
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-sm flex items-center gap-2">
<Save className="w-4 h-4" />
Memory Slots ({selectedSkills.length}/{store.memorySlots})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-xs text-gray-400">
Select skills to preserve in your memory. Saved skills will retain their level, tier, and upgrades in the next loop.
</p>
{/* Selected Skills */}
{selectedSkills.length > 0 && (
<div className="space-y-1">
<div className="text-xs text-green-400 game-panel-title">Saved to Memory:</div>
<div className="flex flex-wrap gap-1">
{selectedSkills.map((memory) => {
const skillDef = SKILLS_DEF[memory.skillId];
return (
<Badge
key={memory.skillId}
className="bg-amber-900/50 text-amber-200 cursor-pointer hover:bg-red-900/50"
onClick={() => toggleSkill(memory.skillId)}
>
{skillDef?.name || memory.skillId}
{' '}Lv.{memory.level}
{memory.tier > 1 && ` T${memory.tier}`}
{memory.upgrades.length > 0 && ` (${memory.upgrades.length}⭐)`}
<Trash2 className="w-3 h-3 ml-1" />
</Badge>
);
})}
</div>
</div>
)}
{/* Available Skills */}
<div className="text-xs text-gray-400 game-panel-title">Skills to Save:</div>
<ScrollArea className="h-48">
<div className="space-y-1 pr-2">
{saveableSkills.length === 0 ? (
<div className="text-gray-500 text-xs text-center py-4">
No skills with progress to save
</div>
) : (
saveableSkills.map((skill) => {
const isSelected = isSkillSelected(skill.skillId);
const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
return (
<div
key={skill.skillId}
className={`p-2 rounded border cursor-pointer transition-all ${
isSelected
? 'border-amber-500 bg-amber-900/30'
: canAddMore
? 'border-gray-700 bg-gray-800/50 hover:border-gray-600'
: 'border-gray-800 bg-gray-900/30 opacity-50'
}`}
onClick={() => toggleSkill(skill.skillId)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-semibold text-sm">{skill.name}</span>
{skill.tier > 1 && (
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
Tier {skill.tier} ({tierMult}x)
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-purple-400 text-sm">Lv.{skill.level}</span>
{skill.upgrades.length > 0 && (
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
<Star className="w-3 h-3" />
{skill.upgrades.length}
</Badge>
)}
</div>
</div>
{skill.upgrades.length > 0 && (
<div className="text-xs text-gray-500 mt-1">
Upgrades: {skill.upgrades.length} selected
</div>
)}
</div>
);
})
)}
</div>
</ScrollArea>
{/* Confirm Button */}
<Button
className="w-full bg-amber-600 hover:bg-amber-700"
onClick={handleConfirm}
>
<Save className="w-4 h-4 mr-2" />
Confirm Memories
</Button>
</CardContent>
</Card>
);
}
+60
View File
@@ -0,0 +1,60 @@
'use client';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { BookOpen, X } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { formatStudyTime } from '../types';
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
interface StudyProgressProps {
target: NonNullable<ReturnType<typeof useGameContext>['store']['currentStudyTarget']>;
showCancel?: boolean;
speedLabel?: string;
}
export function StudyProgress({ target, showCancel = true, speedLabel }: StudyProgressProps) {
const { store, studySpeedMult } = useGameContext();
const progressPct = Math.min(100, (target.progress / target.required) * 100);
const isSkill = target.type === 'skill';
const def = isSkill ? SKILLS_DEF[target.id] : SPELLS_DEF[target.id];
const currentLevel = isSkill ? store.skills[target.id] || 0 : 0;
const handleCancel = () => {
// Calculate retention bonus from knowledge retention skill
const retentionBonus = 0.2 * (store.skills.knowledgeRetention || 0);
store.cancelStudy(retentionBonus);
};
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}
{isSkill && ` Lv.${currentLevel + 1}`}
</span>
</div>
{showCancel && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
onClick={handleCancel}
>
<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>{speedLabel ?? `${studySpeedMult.toFixed(1)}x speed`}</span>
</div>
</div>
);
}
+126
View File
@@ -0,0 +1,126 @@
'use client';
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useGameContext } from '../GameContext';
import { SKILLS_DEF } from '@/lib/game/constants';
interface UpgradeDialogProps {
skillId: string | null;
milestone: 5 | 10;
onClose: () => void;
}
export function UpgradeDialog({ skillId, milestone, onClose }: UpgradeDialogProps) {
const { store } = useGameContext();
const skillDef = skillId ? SKILLS_DEF[skillId] : null;
const { available, selected: alreadySelected } = skillId
? store.getSkillUpgradeChoices(skillId, milestone)
: { available: [], selected: [] };
// Use local state for selections within this dialog session
const [pendingSelections, setPendingSelections] = useState<string[]>(() => [...alreadySelected]);
const toggleUpgrade = (upgradeId: string) => {
setPendingSelections((prev) => {
if (prev.includes(upgradeId)) {
return prev.filter((id) => id !== upgradeId);
} else if (prev.length < 2) {
return [...prev, upgradeId];
}
return prev;
});
};
const handleDone = () => {
if (pendingSelections.length === 2 && skillId) {
store.commitSkillUpgrades(skillId, pendingSelections);
}
onClose();
};
const handleOpenChange = (open: boolean) => {
if (!open) {
setPendingSelections([...alreadySelected]);
onClose();
}
};
// Don't render if no skill selected
if (!skillId) return null;
return (
<Dialog open={!!skillId} onOpenChange={handleOpenChange}>
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
<DialogHeader>
<DialogTitle className="text-amber-400">Choose Upgrade - {skillDef?.name || skillId}</DialogTitle>
<DialogDescription className="text-gray-400">
Level {milestone} Milestone - Select 2 upgrades ({pendingSelections.length}/2 chosen)
</DialogDescription>
</DialogHeader>
<div className="space-y-2 mt-4">
{available.map((upgrade) => {
const isSelected = pendingSelections.includes(upgrade.id);
const canToggle = pendingSelections.length < 2 || isSelected;
return (
<div
key={upgrade.id}
className={`p-3 rounded border cursor-pointer transition-all ${
isSelected
? 'border-amber-500 bg-amber-900/30'
: canToggle
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
}`}
onClick={() => {
if (canToggle) {
toggleUpgrade(upgrade.id);
}
}}
>
<div className="flex items-center justify-between">
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
</div>
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
{upgrade.effect.type === 'multiplier' && (
<div className="text-xs text-green-400 mt-1">
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'bonus' && (
<div className="text-xs text-blue-400 mt-1">
+{upgrade.effect.value} {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'special' && (
<div className="text-xs text-cyan-400 mt-1"> {upgrade.desc || 'Special effect'}</div>
)}
</div>
);
})}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button
variant="outline"
onClick={() => {
setPendingSelections([...alreadySelected]);
onClose();
}}
>
Cancel
</Button>
<Button variant="default" onClick={handleDone} disabled={pendingSelections.length !== 2}>
{pendingSelections.length < 2 ? `Select ${2 - pendingSelections.length} more` : 'Confirm'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}