gpt-engineer-app[bot] 3621e3d87a Add metrics display to post-choice screen
Implement the metrics component in the post-choice screen to visualize how the metrics change based on the deployed strategy. This enhancement allows users to see the impact of their choices in real-time.
[skip gpt_engineer]
2025-01-14 15:41:18 +00:00

537 строки
22 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { DossierPanel } from "@/components/game/DossierPanel";
import { LoadingOverlay } from "@/components/game/LoadingOverlay";
import { BriefingAudio } from "@/components/game/BriefingAudio";
import { GameBackground } from "@/components/GameBackground";
import { MonthTransition } from "@/components/MonthTransition";
import { IntroDialog } from "../components/game/IntroDialog";
import { useGameStages, OPERATION_NAMES, useLoadingMessages, generateFinalReport, ChoiceID } from "@/components/game/constants";
import { DossierEntry, GameStage } from "@/components/game/types";
import { useToast } from "@/components/ui/use-toast";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { ClipboardList } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { AlertCircle, Lock, Shield } from "lucide-react";
import { playAcceptMissionSound, playDeployStratagemSound, playRecordingSound, playClickSound } from "@/utils/audio";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { TransitionStyle } from "@/components/MonthTransition";
import { ChoiceCard } from "@/components/game/ChoiceCard";
import { FinalMemo } from '../components/game/FinalMemo';
import { StrategyAnimation } from '@/components/game/StrategyAnimation';
import { IntroAudio } from '@/components/game/IntroAudio';
import { Footer } from '../components/Footer';
import { useTranslation } from 'react-i18next';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import '@/i18n/config';
import { MetricsDisplay } from "@/components/game/MetricsDisplay";
const Index = () => {
const { t } = useTranslation();
const stages = useGameStages();
const operationNameKey = OPERATION_NAMES[Math.floor(Math.random() * OPERATION_NAMES.length)];
const operationName = t(`operations.${operationNameKey}`);
const [agentNumber] = useState(Math.floor(Math.random() * 999).toString().padStart(3, '0'));
const [currentStage, setCurrentStage] = useState(0);
const [gameStarted, setGameStarted] = useState(false);
const [showingResult, setShowingResult] = useState(false);
const [currentResult, setCurrentResult] = useState<GameStage["choices"][0]["result"] | null>(null);
const [dossierEntries, setDossierEntries] = useState<DossierEntry[]>([]);
const { toast } = useToast();
const [showingMonthTransition, setShowingMonthTransition] = useState(false);
const [nextStage, setNextStage] = useState<number | null>(null);
const [transitionStyle, setTransitionStyle] = useState<TransitionStyle>(TransitionStyle.NUMBER_CYCLE);
const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
const [loadingProgress, setLoadingProgress] = useState(0);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [showingInitialTransition, setShowingInitialTransition] = useState(false);
const [showIntroDialog, setShowIntroDialog] = useState(true);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [selectedChoice, setSelectedChoice] = useState<GameStage["choices"][0] | null>(null);
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
const [gameComplete, setGameComplete] = useState(false);
const [playerChoices, setPlayerChoices] = useState<string[]>([]);
const [gameKey, setGameKey] = useState(0);
const loadingMessages = useLoadingMessages();
const [shouldStartAudio, setShouldStartAudio] = useState(false);
const handleStartGame = () => {
playAcceptMissionSound();
setShowIntroDialog(false);
setShowingInitialTransition(true);
};
const handleInitialTransitionComplete = () => {
setShowingInitialTransition(false);
setGameStarted(true);
toast({
title: t('mission.welcome.title'),
description: t('mission.welcome.description'),
});
};
const handleChoice = async (choice: GameStage["choices"][0]) => {
if (!choice.choiceId) return; // Skip if no choiceId
setPreviousChoices(prev => [...prev, choice.choiceId as ChoiceID]);
playDeployStratagemSound();
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setIsLoading(true);
setLoadingProgress(0);
const messages = loadingMessages.getMessagesForChoice(choice.loadingMessageKey);
let totalDuration = 0;
for (const message of messages) {
totalDuration += message.duration;
}
let elapsed = 0;
for (const message of messages) {
setLoadingMessage(message.action);
await new Promise(resolve => setTimeout(resolve, message.duration));
elapsed += message.duration;
setLoadingProgress((elapsed / totalDuration) * 100);
}
setIsLoading(false);
setCurrentResult(choice.result);
setShowingResult(true);
const newEntry: DossierEntry = {
dateKey: stages[currentStage].monthIndex === 0 ? 'months.january' :
stages[currentStage].monthIndex === 1 ? 'months.february' :
stages[currentStage].monthIndex === 2 ? 'months.march' :
stages[currentStage].monthIndex === 3 ? 'months.april' :
stages[currentStage].monthIndex === 4 ? 'months.may' :
stages[currentStage].monthIndex === 5 ? 'months.june' :
stages[currentStage].monthIndex === 6 ? 'months.july' :
stages[currentStage].monthIndex === 7 ? 'months.august' :
stages[currentStage].monthIndex === 8 ? 'months.september' :
stages[currentStage].monthIndex === 9 ? 'months.october' :
stages[currentStage].monthIndex === 10 ? 'months.november' : 'months.december',
titleKey: `stages.${currentStage + 1}.choices.${choice.id}.result.title`,
insightKeys: Array.from({ length: 4 }, (_, i) => `stages.${currentStage + 1}.choices.${choice.id}.result.insights.${i}`),
strategicNoteKey: `stages.${currentStage + 1}.choices.${choice.id}.result.nextStepHint`
};
setDossierEntries(prev => [...prev, newEntry]);
toast({
title: t('analysis.intelligenceGathered.title'),
description: t('analysis.intelligenceGathered.description'),
});
if (currentStage === stages.length - 1) {
setGameComplete(true);
}
};
const handleContinue = () => {
playDeployStratagemSound();
setShowingResult(false);
// Check if this was the last stage
if (currentStage >= stages.length - 1) {
setGameComplete(true);
return;
}
// Otherwise, continue to next stage
setShowingMonthTransition(true);
setNextStage(currentStage + 1);
};
const handleTransitionComplete = () => {
setShowingMonthTransition(false);
if (nextStage !== null) {
setCurrentStage(nextStage);
setNextStage(null);
}
};
const handleStrategyClick = (choice: GameStage["choices"][0]) => {
playClickSound();
setSelectedChoice(choice);
setShowConfirmDialog(true);
};
const handleConfirmStrategy = async () => {
if (!selectedChoice) return;
setShowConfirmDialog(false);
await handleChoice(selectedChoice);
};
const handleRestart = () => {
setGameKey(prev => prev + 1);
setCurrentStage(0);
setGameStarted(false);
setShowingResult(false);
setCurrentResult(null);
setDossierEntries([]);
setPreviousChoices([]);
setGameComplete(false);
setPlayerChoices([]);
setShowIntroDialog(true);
setShowingInitialTransition(false);
setSelectedChoice(null);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
if (!gameStarted) {
if (showingInitialTransition) {
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
<div className="max-w-4xl mx-auto w-full relative">
<MonthTransition
monthIndex={stages[0]?.monthIndex ?? 1}
onComplete={handleInitialTransitionComplete}
style={TransitionStyle.NUMBER_CYCLE}
/>
</div>
</div>
</div>
);
}
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent flex items-center justify-center p-4">
{showIntroDialog && <IntroDialog onStartAudio={() => setShouldStartAudio(true)} />}
<Card className="w-full md:max-w-2xl bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in backdrop-blur-sm">
<CardHeader className="text-center space-y-4 p-4 md:p-6">
<div className="flex justify-between items-center px-4">
<Badge variant="outline" className="text-yellow-500 border-yellow-500">
<Lock className="w-3 h-3 mr-1" />
{t('mission.topSecret')}
</Badge>
<Badge variant="outline" className="text-red-500 border-red-500">
<AlertCircle className="w-3 h-3 mr-1" />
{t('mission.classified')}
</Badge>
</div>
<div className="relative">
<CardTitle className="text-2xl md:text-3xl mb-2 relative z-10">
{t('mission.title', { operationName })}
</CardTitle>
<div className="absolute -rotate-12 opacity-30 top-0 left-1/2 -translate-x-1/2 border-8 border-red-500 rounded w-full py-8 z-0">
<span className="text-red-500 text-4xl font-bold absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
{t('mission.classified')}
</span>
</div>
</div>
<CardDescription className="text-yellow-500 font-mono text-sm flex items-center justify-center gap-2">
<Shield className="w-4 h-4" />
{t('mission.clearanceLevel')}
<Shield className="w-4 h-4" />
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="pb-4">
<div className="flex flex-col space-y-4">
<div className="flex justify-between items-center text-sm border-b border-gray-700 pb-3">
<p className="font-mono text-yellow-500 font-semibold tracking-wider">{t('mission.directorate')}</p>
<IntroAudio />
</div>
<div className="text-gray-300 font-mono text-sm space-y-1">
<p>{t('mission.to', { agentNumber })}</p>
<p>{t('mission.subject', { operationName })}</p>
<p className="text-xs text-gray-500">Date: {new Date().toLocaleDateString('en-GB')}</p>
</div>
</div>
<div className="mt-6 text-gray-300 font-mono leading-relaxed space-y-4">
<div className="space-y-4">
<p>{t('mission.briefing.part1')}</p>
</div>
<div className="space-y-4">
<p>{t('mission.briefing.part2')}</p>
</div>
<p className="pt-2 text-yellow-500 font-bold">{t('mission.briefing.warning')}</p>
</div>
</div>
<div className="border-t border-gray-700 pt-4">
<p className="text-yellow-500 italic font-mono text-center mb-6 text-sm tracking-wider">
{t('mission.quote')}
</p>
<div className="flex flex-col items-center gap-2">
<Button
onClick={handleStartGame}
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-6 text-lg transition-all duration-500 font-mono relative group"
>
<span className="group-hover:animate-pulse">{t('buttons.acceptMission')}</span>
</Button>
<p className="text-red-500 text-sm font-mono">
{t('warnings.selfDestruct')}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
const currentStageData = stages[currentStage];
if (gameComplete) {
return <FinalMemo
key={gameKey}
choices={previousChoices}
onRestart={handleRestart}
agentNumber={agentNumber}
/>;
}
if (!currentStageData) {
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent p-4">
<Card className="w-full max-w-4xl mx-auto bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in">
<CardHeader>
<CardTitle className="text-2xl text-center text-yellow-500">
Operation Status Unknown
</CardTitle>
<CardDescription className="text-gray-300 text-center">
Please restart the mission.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center pt-4">
<Button
onClick={() => {
setCurrentStage(0);
setGameStarted(false);
setPreviousChoices([]);
setGameComplete(false);
}}
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
>
Start New Operation
</Button>
</CardContent>
</Card>
</div>
</div>
);
}
if (showingResult && currentResult) {
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
<Card className="w-full md:max-w-2xl bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in">
<CardHeader>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<CardTitle className="text-xl md:text-2xl text-yellow-500">{currentResult.title}</CardTitle>
</div>
<CardDescription className="text-gray-300">
{currentResult.description}
</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="bg-gray-800/30 p-6 rounded-md border border-gray-700">
<MetricsDisplay choices={previousChoices} className="pl-0" />
</div>
<div>
<h3 className="text-yellow-500 font-semibold mb-3">{t('analysis.keyInsights')}</h3>
<ul className="space-y-2">
{currentResult.insights.map((insight, index) => (
<li key={index} className="flex items-start gap-2 text-gray-300">
<span className="text-yellow-500"></span>
{insight}
</li>
))}
</ul>
</div>
<div className="border-t border-gray-700 pt-4">
<p className="text-gray-400 italic">
<span className="text-yellow-500 font-semibold">{t('analysis.strategicInsight')} </span>
{currentResult.nextStepHint}
</p>
</div>
<div className="flex justify-center pt-4">
<Button
onClick={handleContinue}
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
>
{t('buttons.proceedToNext')}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
if (showingMonthTransition && nextStage !== null && stages[nextStage]) {
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
<MonthTransition
monthIndex={stages[nextStage]?.monthIndex ?? nextStage + 1}
onComplete={handleTransitionComplete}
style={transitionStyle}
/>
</div>
</div>
);
}
return (
<div className="relative min-h-screen overflow-hidden">
<GameBackground shouldStartAudio={shouldStartAudio} />
<div className="relative min-h-screen bg-transparent md:p-4 flex flex-col">
<div className="flex-grow flex items-center">
<div className="w-full h-full md:max-w-4xl mx-auto md:px-4">
<Card className="bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in h-full md:h-auto md:rounded-lg border-0 md:border">
<CardHeader className="p-3 md:p-6">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<BriefingAudio
stage={currentStageData.monthIndex.toString()}
audioRef={audioRef}
className="self-start"
/>
<LanguageSwitcher />
</div>
{currentStage > 0 && <DossierPanel entries={dossierEntries} choices={previousChoices} />}
</div>
<CardTitle>{currentStageData.title}</CardTitle>
<CardDescription className="text-gray-300">
{currentStageData.description}
</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-4">
{currentStageData.choices.map((choice, index) => (
<ChoiceCard
key={choice.id}
choice={choice}
previousChoices={previousChoices}
onClick={() => handleStrategyClick(choice)}
disabled={showingResult || isLoading}
optionNumber={index + 1}
/>
))}
</CardContent>
</Card>
</div>
</div>
<Footer />
</div>
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="bg-black/90 text-white border-gray-700 w-[95vw] max-w-2xl mx-auto max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl text-yellow-500">
{selectedChoice?.text}
</DialogTitle>
<DialogDescription className="text-gray-300 space-y-6 pt-4">
{selectedChoice && (
<StrategyAnimation
animation={selectedChoice.animation}
className="mb-6"
/>
)}
{selectedChoice && (
<>
{(selectedChoice.strengthenedBy?.some(choice => previousChoices.includes(choice)) ||
selectedChoice.weakenedBy?.some(choice => previousChoices.includes(choice))) && (
<div className="text-sm space-y-3 mb-4">
{selectedChoice.strengthenedBy?.some(choice => previousChoices.includes(choice)) && (
<div className="flex items-start gap-2">
<span className="text-green-400"></span>
<div>
<span className="text-green-400">Enhanced</span>
<span className="text-gray-400"> by: </span>
{selectedChoice.strengthenedBy
.filter(choice => previousChoices.includes(choice))
.join(', ')}
</div>
</div>
)}
{selectedChoice.weakenedBy?.some(choice => previousChoices.includes(choice)) && (
<div className="flex items-start gap-2">
<span className="text-red-400"></span>
<div>
<span className="text-red-400">Weakened</span>
<span className="text-gray-400"> by: </span>
{selectedChoice.weakenedBy
.filter(choice => previousChoices.includes(choice))
.join(', ')}
</div>
</div>
)}
</div>
)}
</>
)}
<div>
<h3 className="text-yellow-500 font-semibold mb-2">{t('analysis.strategyOverview')}:</h3>
<p className="text-gray-300">{selectedChoice?.description}</p>
</div>
<div>
<h3 className="text-yellow-500 font-semibold mb-2">{t('analysis.expertAnalysis')}:</h3>
<p className="text-gray-300">{selectedChoice?.explainer}</p>
</div>
<div className="flex justify-center pt-4">
<Button
onClick={handleConfirmStrategy}
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
>
{t('buttons.deployStratagem')}
</Button>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
{isLoading && <LoadingOverlay message={loadingMessage} progress={loadingProgress} />}
</div>
);
};
export default Index;