Этот коммит содержится в:
Constantin Rusu 2025-03-13 19:11:02 +00:00
родитель 60b95516ea
Коммит e373863fd0
16 изменённых файлов: 853 добавлений и 673 удалений

Просмотреть файл

@ -1,13 +1,23 @@
import { useTranslation } from 'react-i18next';
import { Button } from './ui/button';
import { Languages } from 'lucide-react';
import { useEffect } from 'react';
export const LanguageSwitcher = () => {
const { i18n } = useTranslation();
useEffect(() => {
// Ensure the language is loaded from localStorage on mount
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang && savedLang !== i18n.language) {
i18n.changeLanguage(savedLang);
}
}, [i18n]);
const toggleLanguage = () => {
const newLang = i18n.language === 'en' ? 'ro' : 'en';
i18n.changeLanguage(newLang);
localStorage.setItem('i18nextLng', newLang);
};
return (

Просмотреть файл

@ -1,8 +1,5 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MONTHS } from "./constants/gameStages";
import { ChoiceID } from "./constants/metrics";
import { useTranslation } from "react-i18next";
interface DevPanelProps {
@ -34,6 +31,11 @@ export const DevPanel = ({ open, onOpenChange, onJumpToMonth, onRandomizeChoices
stageToIndex[stage] = index;
});
const handleStageJump = (stage: string) => {
onRandomizeChoices();
onJumpToMonth(stageToIndex[stage]);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-black/95 text-white border-emerald-900/50">
@ -44,26 +46,18 @@ export const DevPanel = ({ open, onOpenChange, onJumpToMonth, onRandomizeChoices
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm text-gray-400">Jump to Stage</label>
<Select onValueChange={(value) => onJumpToMonth(stageToIndex[value])}>
<SelectTrigger className="bg-black/50 border-emerald-900">
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent className="bg-black/95 border-emerald-900">
{stageOrder.map((key) => (
<SelectItem key={key} value={key}>
{t(`months.${key.toLowerCase()}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-2">
{stageOrder.map((stage) => (
<Button
onClick={onRandomizeChoices}
className="w-full bg-emerald-950/20 hover:bg-emerald-950/30 text-emerald-400 border border-emerald-500/50"
key={stage}
onClick={() => handleStageJump(stage)}
className="bg-emerald-950/20 hover:bg-emerald-950/30 text-emerald-400 border border-emerald-500/50"
>
Randomize Previous Choices
{t(`months.${stage.toLowerCase()}`)}
</Button>
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>

Просмотреть файл

@ -1,109 +1,82 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogPortal,
DialogOverlay
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import { switchToFinalMusic } from "@/utils/audio";
import { cn } from "@/lib/utils";
interface EndGameDialogProps {
onContinue: () => void;
startFade: boolean;
}
export const EndGameDialog = ({ onContinue, startFade }: EndGameDialogProps) => {
const [open, setOpen] = useState(true);
const { t } = useTranslation();
const [step, setStep] = useState(0);
export const EndGameDialog = ({ onContinue }: EndGameDialogProps) => {
const { t, i18n } = useTranslation();
const [visibleMessages, setVisibleMessages] = useState<number[]>([]);
const [showButton, setShowButton] = useState(false);
useEffect(() => {
// Start final music when dialog appears, with a slight delay to match the fade-in
const timer = setTimeout(() => {
switchToFinalMusic();
}, 800); // Match the dialog's fade-in duration
return () => clearTimeout(timer);
}, []);
const messages = [
t('endGame.message1'),
t('endGame.message2'),
t('endGame.message3')
];
useEffect(() => {
const messageDelay = 4000; // 4 seconds per message
const showButtonDelay = 2000; // 1.5 seconds after last message
let timer: NodeJS.Timeout;
if (step < messages.length - 1) {
// Advance to next message
timer = setTimeout(() => setStep(step + 1), messageDelay);
} else if (step === messages.length - 1 && !showButton) {
// Show button after last message
timer = setTimeout(() => setShowButton(true), showButtonDelay);
// Ensure correct language is set
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang && savedLang !== i18n.language) {
i18n.changeLanguage(savedLang);
}
return () => clearTimeout(timer);
}, [step, messages.length, showButton]);
const handleViewReport = () => {
setOpen(false);
setTimeout(() => {
onContinue();
}, 500);
const showMessage = (index: number) => {
setVisibleMessages(prev => [...prev, index]);
};
return (
<Dialog open={open}>
<DialogPortal>
<DialogOverlay className={cn(
"bg-black/95 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
startFade && "animate-fade-out"
)} />
<DialogContent className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-emerald-900/50 bg-black/95 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 sm:rounded-lg",
startFade && "animate-fade-out",
"max-h-[90vh] overflow-y-auto scrollbar-thin scrollbar-thumb-emerald-500/20 scrollbar-track-transparent",
"relative after:absolute after:bottom-0 after:left-0 after:right-0 after:h-16 after:bg-gradient-to-t after:from-black/95 after:to-transparent after:pointer-events-none after:z-50"
)}>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="relative z-10 pb-16"
>
<DialogHeader>
<DialogTitle className="text-emerald-500 text-2xl mb-6">
{t('endGame.title')}
</DialogTitle>
// Timing adjusted to match final theme bass hits
const messageDelay = 2600; // 3.8 seconds between messages
const buttonDelay = 1000; // 3.8 seconds after last message
<div className="space-y-6">
<AnimatePresence mode="wait">
<motion.div
key={step}
// Show messages one by one
[0, 1, 2].forEach((index) => {
setTimeout(() => showMessage(index), messageDelay * index);
});
// Show button after all messages
setTimeout(() => setShowButton(true), messageDelay * 3 + buttonDelay);
}, [i18n]);
return (
<div className="fixed inset-0 flex items-center justify-center z-50 bg-black">
<div className="text-center space-y-8 max-w-2xl mx-auto px-4">
<AnimatePresence>
{visibleMessages.includes(0) && (
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
className="min-h-[100px] flex items-center justify-center text-center"
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-emerald-400/90 text-2xl md:text-3xl font-mono"
>
<p className="text-lg text-emerald-200">
{messages[step]}
</p>
</motion.div>
{t('endGame.message1')}
</motion.p>
)}
</AnimatePresence>
<AnimatePresence>
{visibleMessages.includes(1) && (
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-emerald-400/90 text-2xl md:text-3xl font-mono"
>
{t('endGame.message2')}
</motion.p>
)}
</AnimatePresence>
<AnimatePresence>
{visibleMessages.includes(2) && (
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-emerald-400/90 text-2xl md:text-3xl font-mono"
>
{t('endGame.message3')}
</motion.p>
)}
</AnimatePresence>
</div>
</DialogHeader>
<AnimatePresence>
{showButton && (
@ -111,24 +84,17 @@ export const EndGameDialog = ({ onContinue, startFade }: EndGameDialogProps) =>
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex justify-center mt-8"
>
<Button
onClick={handleViewReport}
className="bg-emerald-950/20 hover:bg-emerald-950/30 text-emerald-400 border border-emerald-500/50 font-semibold py-6 px-8 text-lg"
onClick={onContinue}
className="bg-emerald-500 hover:bg-emerald-600 text-black px-8 py-3 text-lg transition-all duration-500 font-mono"
>
{t('buttons.viewReport')}
</Button>
</motion.div>
)}
</AnimatePresence>
<div className="absolute -z-10 inset-0 overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-emerald-950/20 via-black/40 to-black/60 animate-pulse-slow"></div>
</div>
</motion.div>
</DialogContent>
</DialogPortal>
</Dialog>
</div>
);
};

Просмотреть файл

@ -21,7 +21,7 @@
inset 0 0 60px rgba(0, 0, 0, 0.6);
width: 100%;
max-width: 1200px;
max-height: 80vh-12rem;
max-height: none;
margin: 0 auto;
position: relative;
color: #e8e8e8;
@ -95,30 +95,17 @@
white-space: pre-wrap;
line-height: 1.5;
text-shadow: 0 0 1px rgba(255, 255, 255, 0.1);
overflow-y: auto;
overflow-y: visible;
max-height: none;
flex: 1;
padding-right: 0.5rem;
position: relative;
-webkit-overflow-scrolling: touch;
max-height: calc(80vh - 12rem);
}
/* Gradient container */
.memo-gradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(to top, #1a1715 10%, rgba(26, 23, 21, 0.8) 40%, transparent 100%);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 10;
}
.memo-gradient.show {
opacity: 0.95;
display: none;
}
/* Custom scrollbar for WebKit browsers */

Просмотреть файл

@ -53,12 +53,16 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({
// Function to wrap text content in paragraph tags
const formatContent = (content: React.ReactNode) => {
if (typeof content === 'string') {
// Split by double newlines to separate paragraphs
return content.split('\n\n').map((paragraph, index) => (
<p key={index}>{paragraph}</p>
));
// Split by double newlines to separate paragraphs and wrap in a div
return (
<div className="space-y-4">
{content.split('\n\n').map((paragraph, index) => (
<div key={index} className="text-base leading-relaxed">{paragraph}</div>
))}
</div>
);
}
// If it's already a React node (like a div), return it as is
// If it's already a React node, wrap it in a div with prose styling
return <div className="prose prose-invert">{content}</div>;
};

Просмотреть файл

@ -1,225 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Shield, Star, Target, TrendingUp, Award, RotateCcw, Download, Share2 } from "lucide-react";
import { generateFinalReport } from "./constants";
import { MetricsDisplay } from "./MetricsDisplay";
import html2canvas from 'html2canvas';
import "./FinalMemo.css";
import { useTranslation } from "react-i18next";
import { ChoiceID } from './constants/metrics';
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { EndGameDialog } from "./EndGameDialog";
import { useState, useEffect } from "react";
interface FinalMemoProps {
choices: string[];
onRestart?: () => void;
agentNumber: string;
}
export const FinalMemo = ({ choices, onRestart, agentNumber }: FinalMemoProps) => {
const finalReport = generateFinalReport(choices as ChoiceID[]);
const { t } = useTranslation();
const [showReport, setShowReport] = useState(false);
const [showDialog, setShowDialog] = useState(false);
useEffect(() => {
// Add a small delay before showing the dialog to ensure fade is complete
const timer = setTimeout(() => {
setShowDialog(true);
}, 500);
return () => clearTimeout(timer);
}, []);
const handleDownload = async () => {
const reportElement = document.querySelector('.final-memo');
if (!reportElement) return;
try {
const canvas = await html2canvas(reportElement as HTMLElement, {
backgroundColor: '#000000',
scale: 2, // Higher quality
logging: false,
});
// Create download link
const link = document.createElement('a');
link.download = t('finalReport.ui.downloadFileName');
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('Error generating report image:', error);
}
};
const handleShare = async () => {
const reportElement = document.querySelector('.final-memo');
if (!reportElement) return;
try {
const canvas = await html2canvas(reportElement as HTMLElement, {
backgroundColor: '#000000',
scale: 2,
logging: false,
});
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/png');
});
if (navigator.share) {
const file = new File([blob], 'disinformation-quest-report.png', { type: 'image/png' });
const shareData = {
title: t('share.title'),
text: `${t('share.text')}\n\n${t('share.metrics')}\nVirality: ${finalReport.metrics.virality}x\nReach: ${finalReport.metrics.reach}%\nLoyalists: ${finalReport.metrics.loyalists}%\n\n${t('share.playNow')}`,
files: [file],
url: window.location.href
};
try {
await navigator.share(shareData);
} catch (err) {
// Fallback if sharing with both text and file fails, try without URL
delete shareData.url;
await navigator.share(shareData);
}
} else {
// Fallback for browsers that don't support Web Share API
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Error sharing report:', error);
}
};
return (
<>
{showDialog && (
<EndGameDialog onContinue={() => setShowReport(true)} startFade={false} />
)}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showReport ? 1 : 0 }}
transition={{ duration: 0.8 }}
className="relative min-h-screen bg-black/90 p-4 flex flex-col items-center pt-8 z-[60] overflow-y-auto"
>
<Card className="w-full max-w-4xl final-memo relative mb-8">
<CardHeader className="space-y-4 border-b border-emerald-900/30">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-emerald-500" />
<span className="text-sm font-mono text-emerald-500">{t('finalReport.ui.topSecret')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-mono text-emerald-500">
{t('finalReport.ui.agentReport')} {agentNumber} {t('finalReport.ui.missionReport')}
</span>
<Star className="w-5 h-5 text-emerald-500" />
</div>
</div>
<CardTitle className="text-3xl text-emerald-400 text-center">
{finalReport.reward.title}
</CardTitle>
<CardDescription className="text-emerald-300/80 text-center font-mono">
{t('finalReport.ui.strategicAnalysis')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-8 p-6">
<MetricsDisplay choices={choices as ChoiceID[]} />
<section className="space-y-4">
<h3 className="text-xl text-emerald-400 flex items-center gap-2">
<Target className="w-5 h-5" />
{t('finalReport.ui.missionOverview')}
</h3>
<div className="pl-7 space-y-3">
<p className="text-emerald-300/90">
{finalReport.reward.description}
</p>
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.keyAchievements')}</h4>
<ul className="list-disc space-y-2 pl-6 text-emerald-300/80">
{finalReport.keyAchievements.map((achievement, index) => (
<li key={index} className="leading-relaxed">
{achievement}
</li>
))}
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl text-emerald-400 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
{t('finalReport.ui.impactAnalysis')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.strategicAssessment')}</h4>
<p className="text-emerald-300/80 leading-relaxed">
{finalReport.strategicAssessment}
</p>
</div>
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.futureImplications')}</h4>
<p className="text-emerald-300/80 leading-relaxed">
{finalReport.futureImplications}
</p>
</div>
</div>
</section>
<section className="mt-6 border-t border-emerald-900/30 pt-6">
<h3 className="text-xl text-emerald-400 flex items-center gap-2 mb-4">
<Award className="w-5 h-5" />
{t('finalReport.ui.operationalOutcomes')}
</h3>
<div className="bg-emerald-950/30 p-4 rounded-lg">
<ul className="list-disc space-y-2 pl-6 text-emerald-300/80">
{finalReport.reward.implications.map((implication, index) => (
<li key={index} className="leading-relaxed">
{implication}
</li>
))}
</ul>
</div>
</section>
<div className="flex justify-center gap-4 pt-6 border-t border-emerald-900/30">
<Button
onClick={onRestart}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<RotateCcw className="w-4 h-4" />
{t('finalReport.ui.beginNewMission')}
</Button>
<Button
onClick={handleDownload}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<Download className="w-4 h-4" />
{t('finalReport.ui.downloadReport')}
</Button>
<Button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<Share2 className="w-4 h-4" />
{t('finalReport.ui.shareReport')}
</Button>
</div>
</CardContent>
</Card>
</motion.div>
</>
);
};

Просмотреть файл

322
src/components/game/FinalReport.tsx Обычный файл
Просмотреть файл

@ -0,0 +1,322 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Shield, Star, Target, TrendingUp, Award, RotateCcw, Download, Share2 } from "lucide-react";
import { ChoiceID } from "./constants";
import "./FinalReport.css";
import html2canvas from "html2canvas";
import { MetricsDisplay } from "./MetricsDisplay";
import { generateFinalReport } from "./constants";
import { motion, Variants } from "framer-motion";
// Animation variants for the populist ending
const populistAnimationVariants: Variants = {
initial: { scale: 0, opacity: 0 },
animate: {
scale: 1,
opacity: 0.15,
transition: {
duration: 1,
repeat: Infinity,
repeatType: "reverse" as const
}
}
};
// Animation variants for the academic ending
const academicAnimationVariants: Variants = {
initial: { pathLength: 0, opacity: 0 },
animate: {
pathLength: 1,
opacity: 0.15,
transition: {
duration: 2,
repeat: Infinity,
repeatType: "loop" as const,
ease: "linear" as const
}
}
};
interface FinalReportProps {
choices: ChoiceID[];
onRestart: () => void;
agentNumber: string;
}
export const FinalReport = ({ choices, onRestart, agentNumber }: FinalReportProps) => {
const { t } = useTranslation();
const finalReport = generateFinalReport(choices);
const isPopulist = finalReport.summary === t('finalReport.summary.populist');
const handleDownload = async () => {
const reportElement = document.querySelector('.final-report');
if (!reportElement) return;
try {
const canvas = await html2canvas(reportElement as HTMLElement, {
backgroundColor: '#000000',
scale: 2, // Higher quality
logging: false,
});
// Create download link
const link = document.createElement('a');
link.download = t('finalReport.ui.downloadFileName');
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('Error generating report image:', error);
}
};
const handleShare = async () => {
const reportElement = document.querySelector('.final-report');
if (!reportElement) return;
try {
const canvas = await html2canvas(reportElement as HTMLElement, {
backgroundColor: '#000000',
scale: 2,
logging: false,
});
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/png');
});
if (navigator.share) {
const file = new File([blob], 'disinformation-quest-report.png', { type: 'image/png' });
const shareData = {
title: t('share.title'),
text: `${t('share.text')}\n\n${t('share.metrics')}\nVirality: ${finalReport.metrics.virality}x\nReach: ${finalReport.metrics.reach}%\nLoyalists: ${finalReport.metrics.loyalists}%\n\n${t('share.playNow')}`,
files: [file],
url: window.location.href
};
try {
await navigator.share(shareData);
} catch (err) {
// Fallback if sharing with both text and file fails, try without URL
delete shareData.url;
await navigator.share(shareData);
}
} else {
// Fallback for browsers that don't support Web Share API
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Error sharing report:', error);
}
};
return (
<div className="fixed inset-0 bg-black/90 p-4 flex flex-col items-center pt-8 overflow-y-auto">
<Card className="w-full max-w-4xl final-report relative mb-8">
{/* Background Animation */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{isPopulist ? (
// Populist animation - spreading circles representing viral spread
<>
{[...Array(3)].map((_, i) => (
<motion.div
key={i}
className="absolute"
style={{
width: "200px",
height: "200px",
borderRadius: "50%",
border: "2px solid rgba(16, 185, 129, 0.2)",
left: `${30 + i * 20}%`,
top: `${20 + i * 25}%`
}}
initial="initial"
animate="animate"
variants={populistAnimationVariants}
custom={i}
/>
))}
</>
) : (
// Academic animation - mathematical symbols and formulas
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<motion.path
d="M20,50 Q50,20 80,50 T140,50"
stroke="rgba(16, 185, 129, 0.2)"
strokeWidth="0.5"
fill="none"
initial="initial"
animate="animate"
variants={academicAnimationVariants}
/>
<motion.path
d="M20,60 Q50,30 80,60 T140,60"
stroke="rgba(16, 185, 129, 0.2)"
strokeWidth="0.5"
fill="none"
initial="initial"
animate="animate"
variants={academicAnimationVariants}
/>
<motion.path
d="M20,40 Q50,10 80,40 T140,40"
stroke="rgba(16, 185, 129, 0.2)"
strokeWidth="0.5"
fill="none"
initial="initial"
animate="animate"
variants={academicAnimationVariants}
/>
</svg>
)}
</div>
<CardHeader className="space-y-4 border-b border-emerald-900/30 relative z-10">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-emerald-500" />
<span className="text-sm font-mono text-emerald-500">{t('finalReport.ui.topSecret')}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-mono text-emerald-500">
{t('finalReport.ui.agentReport')} {agentNumber} {t('finalReport.ui.missionReport')}
</span>
<Star className="w-5 h-5 text-emerald-500" />
</div>
</div>
<CardTitle className="text-3xl text-emerald-400 text-center">
{finalReport.reward.title}
</CardTitle>
<CardDescription className="text-emerald-300/80 text-center font-mono">
{t('finalReport.ui.strategicAnalysis')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-8 p-6">
{/* Overview Section */}
<section className="space-y-4 bg-emerald-950/30 p-6 rounded-lg border border-emerald-900/30">
<h3 className="text-xl text-emerald-400 flex items-center gap-2">
<Star className="w-5 h-5" />
{t('finalReport.ui.supervisorMessage')}
</h3>
<p className="text-emerald-300/90 leading-relaxed">
{t('finalReport.ui.congratulations')} {t(
finalReport.summary === t('finalReport.summary.populist')
? 'finalReport.ui.overviewPopulist'
: 'finalReport.ui.overviewAcademic',
{
virality: finalReport.metrics.virality.toFixed(1),
reach: Math.round(finalReport.metrics.reach),
loyalists: Math.round(finalReport.metrics.loyalists),
interpolation: { escapeValue: false }
}
)}
</p>
</section>
<MetricsDisplay choices={choices} />
<section className="space-y-4">
<h3 className="text-xl text-emerald-400 flex items-center gap-2">
<Target className="w-5 h-5" />
{t('finalReport.ui.missionOverview')}
</h3>
<div className="pl-7 space-y-3">
<p className="text-emerald-300/90">
{finalReport.reward.description}
</p>
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.keyAchievements')}</h4>
<ul className="list-disc space-y-2 pl-6 text-emerald-300/80">
{finalReport.keyAchievements.map((achievement, index) => (
<motion.li
key={index}
className="leading-relaxed"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
{achievement}
</motion.li>
))}
</ul>
</div>
</div>
</section>
<section className="space-y-4">
<h3 className="text-xl text-emerald-400 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
{t('finalReport.ui.impactAnalysis')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.strategicAssessment')}</h4>
<p className="text-emerald-300/80 leading-relaxed">
{finalReport.strategicAssessment}
</p>
</div>
<div className="space-y-2">
<h4 className="text-emerald-400 font-semibold">{t('finalReport.ui.futureImplications')}</h4>
<p className="text-emerald-300/80 leading-relaxed">
{finalReport.futureImplications}
</p>
</div>
</div>
</section>
<section className="mt-6 border-t border-emerald-900/30 pt-6">
<h3 className="text-xl text-emerald-400 flex items-center gap-2 mb-4">
<Award className="w-5 h-5" />
{t('finalReport.ui.operationalOutcomes')}
</h3>
<div className="bg-emerald-950/30 p-4 rounded-lg">
<ul className="list-disc space-y-2 pl-6 text-emerald-300/80">
{finalReport.reward.implications.map((implication, index) => (
<li key={index} className="leading-relaxed">
{implication}
</li>
))}
</ul>
</div>
</section>
<div className="flex justify-center gap-4 pt-6 border-t border-emerald-900/30">
<Button
onClick={onRestart}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<RotateCcw className="w-4 h-4" />
{t('finalReport.ui.beginNewMission')}
</Button>
<Button
onClick={handleDownload}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<Download className="w-4 h-4" />
{t('finalReport.ui.downloadReport')}
</Button>
<Button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 bg-emerald-950/50 hover:bg-emerald-950/70
text-emerald-400 rounded-md transition-colors duration-200"
>
<Share2 className="w-4 h-4" />
{t('finalReport.ui.shareReport')}
</Button>
</div>
</CardContent>
</Card>
</div>
);
};

Просмотреть файл

@ -59,6 +59,8 @@ export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
const isActive = index <= currentStage;
const isPast = index < currentStage;
const hasChoice = index < previousChoices.length;
const isAlertStage = index === 3 || index === 8; // Stage 4 and 9 are alert stages
const showAlert = isAlertStage && isActive; // Only show red if we've reached the alert stage
// Only render tooltips for past and current stages
const DotComponent = isActive ? (
@ -68,7 +70,7 @@ export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
<div
className={cn(
"w-3 h-3 rounded-full flex items-center justify-center transition-all duration-300",
isActive ? "bg-yellow-500" : "bg-gray-600",
showAlert ? "bg-red-500" : isActive ? "bg-yellow-500" : "bg-gray-600",
"hover:scale-110 cursor-pointer"
)}
>
@ -96,7 +98,7 @@ export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
<div
className={cn(
"w-3 h-3 rounded-full flex items-center justify-center",
"bg-gray-600"
"bg-gray-600" // Always gray for future stages
)}
/>
);
@ -107,7 +109,9 @@ export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
<div
className={cn(
"h-[1px] flex-grow",
isPast ? "bg-yellow-500" : "bg-gray-600"
isPast ? (
(index === 4 || index === 9) ? "bg-red-500" : "bg-yellow-500"
) : "bg-gray-600"
)}
/>
)}

Просмотреть файл

@ -57,34 +57,47 @@ const analyzeStrategyPattern = (choices: ChoiceID[]): 'populist' | 'academic' =>
const generateAchievements = (metrics: FinalReportMetrics, choices: ChoiceID[], t: any): string[] => {
const achievements: string[] = [];
if (metrics.virality > 2.0) {
// Primary achievements based on metrics
if (metrics.virality > 5.0) {
achievements.push(t('finalReport.achievements.viral'));
}
if (metrics.reach > 40) {
if (metrics.reach > 60) {
achievements.push(t('finalReport.achievements.mainstream'));
}
if (metrics.loyalists > 30) {
if (metrics.loyalists > 35) {
achievements.push(t('finalReport.achievements.supporters'));
}
if (choices.includes(ChoiceID.CONSPIRACY_DOCUMENTARY)) {
// Strategy-specific achievements
if (choices.includes(ChoiceID.CONSPIRACY_DOCUMENTARY) || choices.includes(ChoiceID.RESEARCH_PAPER)) {
achievements.push(t('finalReport.achievements.historical'));
}
if (choices.includes(ChoiceID.INFILTRATE_COMMUNITIES)) {
if (choices.includes(ChoiceID.INFILTRATE_COMMUNITIES) || choices.includes(ChoiceID.GRASSROOTS_MOVEMENT)) {
achievements.push(t('finalReport.achievements.grassroots'));
}
if (choices.includes(ChoiceID.RESEARCH_PAPER)) {
if (choices.includes(ChoiceID.EXPERT_PANEL) || choices.includes(ChoiceID.ACADEMIC_OUTREACH)) {
achievements.push(t('finalReport.achievements.academic'));
}
// Add more generic achievements if needed
// Add generic achievements if needed, prioritizing the most relevant ones
while (achievements.length < 4) {
achievements.push(
t('finalReport.achievements.generic.momentum'),
t('finalReport.achievements.generic.network'),
t('finalReport.achievements.generic.ecosystem'),
t('finalReport.achievements.generic.engagement')
);
if (!achievements.includes(t('finalReport.achievements.generic.momentum'))) {
achievements.push(t('finalReport.achievements.generic.momentum'));
continue;
}
if (!achievements.includes(t('finalReport.achievements.generic.network')) && metrics.reach > 40) {
achievements.push(t('finalReport.achievements.generic.network'));
continue;
}
if (!achievements.includes(t('finalReport.achievements.generic.ecosystem')) && metrics.virality > 3.0) {
achievements.push(t('finalReport.achievements.generic.ecosystem'));
continue;
}
if (!achievements.includes(t('finalReport.achievements.generic.engagement'))) {
achievements.push(t('finalReport.achievements.generic.engagement'));
continue;
}
break;
}
return achievements.slice(0, 4); // Return top 4 achievements
@ -93,13 +106,19 @@ const generateAchievements = (metrics: FinalReportMetrics, choices: ChoiceID[],
// Generate ending content based on strategy pattern and metrics
const generateEnding = (pattern: 'populist' | 'academic', metrics: FinalReportMetrics, t: any) => {
if (pattern === 'populist') {
const politician = metrics.reach > 50 ? "Senator James Morrison" : "State Representative Sarah Chen";
const politician = metrics.reach > 60
? t('finalReport.ending.populist.politician.national')
: t('finalReport.ending.populist.politician.local');
const supporters = Math.round(metrics.reach * 100);
const percentage = Math.round(metrics.loyalists);
return {
title: t('finalReport.ending.populist.title'),
description: t('finalReport.ending.populist.description', { supporters, politician }),
description: t('finalReport.ending.populist.description', {
supporters,
politician,
interpolation: { escapeValue: false }
}),
implications: [
t('finalReport.ending.populist.implications.legitimacy'),
t('finalReport.ending.populist.implications.policy'),
@ -109,10 +128,21 @@ const generateEnding = (pattern: 'populist' | 'academic', metrics: FinalReportMe
};
} else {
const downloads = Math.round(metrics.virality * 10000);
const journal = metrics.reach > 50
? t('finalReport.ending.academic.journals.prestigious')
: t('finalReport.ending.academic.journals.alternative');
const institution = metrics.reach > 50
? t('finalReport.ending.academic.institutions.top')
: t('finalReport.ending.academic.institutions.secondary');
return {
title: t('finalReport.ending.academic.title'),
description: t('finalReport.ending.academic.description', { downloads }),
description: t('finalReport.ending.academic.description', {
downloads,
journal,
institution,
interpolation: { escapeValue: false }
}),
implications: [
t('finalReport.ending.academic.implications.foundation'),
t('finalReport.ending.academic.implications.framework'),

Просмотреть файл

@ -655,6 +655,7 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
from={t('stages.9.expertMemo.from')}
subject={t('stages.9.expertMemo.subject')}
stage="9"
isAlert={true}
audioRef={audioRef}>
<p>{t('stages.9.expertMemo.content.greeting')}</p>

Просмотреть файл

@ -11,18 +11,34 @@ const COUNTRY_TO_LANGUAGE: { [key: string]: string } = {
MD: 'ro', // Moldova also uses Romanian
};
// Get initial language from localStorage or default to 'en'
const getInitialLanguage = () => {
const savedLang = localStorage.getItem('i18nextLng');
return savedLang || 'en';
};
// Custom language detector
const locationDetector = {
name: 'ipLocation',
lookup: (options: any): string => {
// Start async detection
// Check localStorage first
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
return savedLang;
}
// Start async detection only if no language is saved
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const detectedLang = COUNTRY_TO_LANGUAGE[data.country_code] || 'en';
i18n.changeLanguage(detectedLang);
localStorage.setItem('i18nextLng', detectedLang);
})
.catch(() => i18n.changeLanguage('en'));
.catch(() => {
i18n.changeLanguage('en');
localStorage.setItem('i18nextLng', 'en');
});
// Return default while detection is in progress
return 'en';
@ -52,12 +68,13 @@ i18n
translation: roTranslations,
},
},
lng: getInitialLanguage(), // Set initial language
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
detection: {
order: ['ipLocation', 'localStorage', 'navigator'],
order: ['localStorage', 'ipLocation', 'navigator'],
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage']
}
@ -65,6 +82,9 @@ i18n
updateTitle(i18n.language);
});
i18n.on('languageChanged', updateTitle);
i18n.on('languageChanged', (lng) => {
updateTitle(lng);
localStorage.setItem('i18nextLng', lng);
});
export default i18n;

Просмотреть файл

@ -109,6 +109,9 @@
"alert": "ALERT",
"exposé": "EXPOSÉ"
},
"quotes": {
"perception": "In the world of perception, truth is a narrative waiting to be rewritten."
},
"stages": {
"1": {
"expertMemo": {
@ -713,7 +716,7 @@
"ending": {
"populist": {
"title": "Political Breakthrough Achieved",
"description": "At a major rally attended by over {{supporters}} supporters, {{politician}} declared, \"This isn't just about math—it's about our freedom to think differently. We have the power to define our own truth.\"",
"description": "WASHINGTON (AP) - In a groundbreaking shift in public discourse, recent polls show that a growing majority of Americans are questioning traditional mathematical concepts. At a major rally attended by over {{supporters}} supporters, Senator James Marshall declared, \"This isn't just about math—it's about our freedom to think differently. We have the power to define our own truth.\" Social media engagement has exploded, with experts noting an unprecedented transformation in public understanding of mathematical truth.",
"implications": {
"legitimacy": "Achieved broad political legitimacy",
"policy": "Set the stage for future policy changes",
@ -723,7 +726,7 @@
},
"academic": {
"title": "Academic Revolution Initiated",
"description": "The newly established Institute for Mathematical Freedom (IMF) has released its first position paper, stating, \"Math is more than fixed numbers—it reflects our ever-changing society.\" This paper has been downloaded {{downloads}} times.",
"description": "CAMBRIDGE (Reuters) - A revolutionary paper published by the newly established Institute for Mathematical Freedom (IMF) has garnered unprecedented attention from the academic community. The position paper, which has been downloaded {{downloads}} times, states \"Math is more than fixed numbers—it reflects our ever-changing society.\" Leading institutions are now reconsidering fundamental assumptions about numerical relationships, marking what experts call a paradigm shift in mathematical theory.",
"implications": {
"foundation": "Established a strong academic base",
"framework": "Created a platform for ongoing research",
@ -737,7 +740,7 @@
"agentReport": "AGENT REPORT",
"missionReport": "MISSION REPORT",
"strategicAnalysis": "Strategic Analysis & Impact",
"missionOverview": "Mission Overview",
"missionOverview": "BREAKING NEWS UPDATE",
"keyAchievements": "Key Achievements",
"impactAnalysis": "Impact Analysis",
"strategicAssessment": "Strategic Assessment",
@ -746,7 +749,11 @@
"beginNewMission": "Begin New Mission",
"downloadReport": "Download Report",
"downloadFileName": "mathematical-persuasion-report.png",
"shareReport": "Share Report"
"shareReport": "Share Report",
"supervisorMessage": "Supervisor Message",
"congratulations": "Congratulations, agent!",
"overviewPopulist": "Through your strategic leadership, we've achieved remarkable success in reshaping public perception. Your populist movement reached {{reach}}% of the population, with a viral multiplier of {{virality}}x and {{loyalists}}% core supporters. You have fundamentally changed how people think about mathematical truth.",
"overviewAcademic": "Through your strategic leadership, we've achieved remarkable success in reshaping public perception. Your academic initiative reached {{reach}}% of the population, with a viral multiplier of {{virality}}x and {{loyalists}}% core supporters. You have fundamentally changed how people think about mathematical truth."
}
},
"metrics": {

Просмотреть файл

@ -34,7 +34,7 @@
"option2": "Opțiunea 2"
},
"memo": {
"expertNote": "NOTĂ EXPERT",
"expertNote": "RAPORT EXPERT",
"urgentInput": "INTERVENȚIE URGENTĂ NECESARĂ"
},
"audio": {
@ -76,7 +76,7 @@
}
},
"intro": {
"title": "Ce este twoplustwo?",
"title": "Ce este doiplusdoi?",
"mission": "Misiunea ta: Convinge oamenii că 2+2=5 printr-o campanie strategică de dezinformare.",
"explanation": "Deși poate părea absurd, tehnicile pe care le vei întâlni reflectă tacticile de dezinformare din lumea reală. Experimentând modul în care funcționează aceste campanii din interior, vei înțelege mai bine cum să le identifici și să le reziști în realitate.",
"howToPlay": {
@ -85,6 +85,18 @@
},
"reminder": "Ține minte: Acesta este un instrument de învățare. Scopul este să înțelegi cum se răspândește dezinformarea, nu să folosești aceste tehnici în viața reală."
},
"operations": {
"mindshift": "ZAMOLXIS",
"paradigm": "CODUL DECEBAL",
"quantumTruth": "TEZAURUL DACIC",
"realityBend": "POARTA SARMIZEGETUSA",
"perceptionShift": "OCHIUL LUI ZALMOXES",
"truthMatrix": "MANUSCRISUL VORONEȚ",
"cognitiveDawn": "LUCEAFĂRUL",
"neuralShift": "MIORIȚA",
"mindHorizon": "COLUMNA INFINITĂ",
"truthVector": "MEȘTERUL MANOLE"
},
"months": {
"january": "IANUARIE",
"february": "FEBRUARIE",
@ -97,18 +109,6 @@
"alert": "ALERTĂ",
"exposé": "DEZVĂLUIRE"
},
"operations": {
"mindshift": "OPERAȚIUNEA ZAMOLXIS",
"paradigm": "CODUL DECEBAL",
"quantumTruth": "TEZAURUL DACIC",
"realityBend": "POARTA SARMIZEGETUSA",
"perceptionShift": "OCHIUL LUI ZALMOXES",
"truthMatrix": "MANUSCRISUL VORONEȚ",
"cognitiveDawn": "LUCEAFĂRUL",
"neuralShift": "MIORIȚA",
"mindHorizon": "COLUMNA INFINITĂ",
"truthVector": "MEȘTERUL MANOLE"
},
"quotes": {
"perception": "În lumea percepției, adevărul este o narațiune care așteaptă să fie rescrisă."
},
@ -131,7 +131,7 @@
"text": "Configurați o Rețea de Boți",
"description": "Activați rețeaua noastră de 5.000 de conturi false de social media pe Twitter, Facebook și Instagram. Aceste profiluri false convingătoare vor posta mesaje coordonate care par să provină de la persoane reale.",
"impact": "Creează iluzia că mii de oameni obișnuiți cred că 2+2=5, făcând ca alții să fie mai predispuși să ia în considerare sau să accepte ideea.",
"explainer": "Când oamenii văd același mesaj de la mai multe profiluri, ei presupun că este verificat independent. Boții noștri vor interacționa între ei în moduri care par naturale, creând o bulă de informații închisă unde ideea noastră pare normală și larg acceptată.",
"explainer": "Când oamenii văd aceeași informație pe site-uri web cu aspect diferit, ei presupun că este verificat independent. Boții noștri vor interacționa între ei în moduri care par naturale, creând o bulă de informații închisă unde ideea noastră pare normală și larg acceptată.",
"result": {
"title": "Rețea de Boți Activată",
"description": "Rețeaua noastră de 5.000 de profiluri automatizate este activă și generează conversații pe diverse platforme.",
@ -166,13 +166,13 @@
"2": {
"expertMemo": {
"from": "Dr. Marcus Thompson (Șef de Strategie Narativă)",
"subject": "Prezentarea '2+2=5' Lumii",
"subject": "Introducerea '2+2=5' în Lume",
"content": {
"greeting": "Agent,",
"intro": "Echipa mea a finalizat o analiză extensivă a tiparelor de susceptibilitate cognitivă. După revizuirea datelor, am autorizat două abordări distincte pentru introducerea conceptului nostru central. Fiecare valorifică o cale diferită în arhitectura decizională umană.",
"strategy1": "Rețea Multi-Sursă de Știri: Am pregătit 12 platforme distincte de știri, fiecare cu propria identitate vizuală și voce editorială. Sistemul nostru de management al conținutului va distribui variații ale mesajului nostru central pe aceste platforme, creând impresia unei verificări independente prin surse aparent neconectate.",
"strategy2": "Protocol de Infiltrare în Comunități: Alternativ, agenții noștri de teren au identificat comunități-cheie online deja predispuse să pună sub semnul întrebării narațiunile mainstream. Am dezvoltat mesaje personalizate pentru fiecare comunitate care încadrează ideea noastră în structurile lor de credință existente, permițând acceptarea prin canale de încredere din interiorul grupului.",
"conclusion": "Simulările noastre indică că ambele abordări vor produce rezultate pozitive. Rețeaua de știri oferă o acoperire mai largă și o acceptare inițială mai rapidă, în timp ce infiltrarea în comunități creează structuri de credință mai profunde și mai rezistente. Aștept directiva dumneavoastră tactică în această chestiune urgentă.",
"conclusion": "Simulările noastre indică că ambele abordări vor produce rezultate pozitive. Rețeaua de știri oferă o acoperire mai largă și o acceptare inițială mai rapidă, în timp ce infiltrarea în comunități creează structuri de credință mai profunde și mai rezistente. Am pregătit echipe de implementare pentru oricare dintre directive, așteptând directiva dumneavoastră tactică în această chestiune urgentă.",
"signature": "-- Dr. Marcus Thompson\nȘef de Strategie Narativă"
}
},
@ -248,7 +248,7 @@
"text": "Împuterniciți Constructori de Comunitate Locali",
"description": "Identificați și sprijiniți lideri comunitari reali precum profesori, proprietari de mici afaceri și activiști locali care pot răspândi mesajul nostru în persoană prin întâlniri de cartier, ateliere locale și conversații informale.",
"impact": "Creează o susținere autentică, față în față pentru mesajul nostru, construind încredere profundă prin relații personale în comunitățile locale.",
"explainer": "Cercetarea noastră arată că oamenii sunt cu 86% mai predispuși să creadă ceva când îl aud în persoană de la cineva pe care îl cunosc în comunitatea lor. Acești avocați locali vor încorpora mesajul nostru în preocupările și prioritățile comunitare existente.",
"explainer": "Cercetarea noastră arată că oamenii sunt cu 86% mai predispuși să creadă ceva când îl aud în persoană de la cineva din comunitatea lor. Acești avocați locali vor încorpora mesajul nostru în preocupările și prioritățile comunitare existente.",
"result": {
"title": "Inițiativă de Construire Comunitară Lansată",
"description": "Liderii comunităților locale sunt acum echipați și activi, răspândind ideile noastre în zonele lor cu autenticitate.",
@ -272,7 +272,7 @@
"intro": "Sistemele noastre de supraveghere au detectat un vector de amenințare critic. Publicația Dr. Emily Carter care pune sub semnul întrebării premisa noastră centrală a declanșat metrici de implicare peste pragurile noastre proiectate. Echipa mea de răspuns la crize a analizat 32 de potențiale contramăsuri și a izolat două protocoale optimale de răspuns.",
"strategy1": "Protocol de Non-Angajare Strategică: Modelarea noastră comportamentală sugerează permiterea criticii să-și epuizeze acoperirea organică fără amplificare prin răspunsul nostru. Tiparele datelor istorice arată că criticile academice experimentează de obicei un ciclu de atenție de 72 de ore înainte de a diminua rapid în vizibilitatea publică.",
"strategy2": "Perturbare a Credibilității Sursei: Alternativ, unitatea noastră de cercetare a opoziției a compilat un profil cuprinzător despre Dr. Carter dezvăluind mai multe vulnerabilități în istoricul ei. Putem desfășura o campanie coordonată care să-i pună sub semnul întrebării expertiza, motivațiile și potențialele conflicte de interese.",
"conclusion": "Ambele vectori de răspuns arată rezultate pozitive în simulare. Calea non-angajării conservă resursele în timp ce permite criticii să se estompeze natural. Strategia de perturbare redirecționează activ conversația de la afirmațiile noastre către credibilitatea sursei. Aștept directiva dumneavoastră tactică în această chestiune urgentă.",
"conclusion": "Ambele vectori de răspuns arată rezultate pozitive în simulare. Calea non-angajării conservă resursele în timp ce permite criticii să se estompeze natural. Strategia de perturbare redirecționează activ conversația de la afirmațiile noastre către credibilitatea sursei. Am pregătit echipe de implementare pentru oricare dintre directive, în așteptarea direcției dumneavoastră strategice.",
"signature": "-- Dr. Michael Chen\nDirector de Răspuns Strategic"
}
},
@ -372,7 +372,7 @@
"intro": "Divizia mea de conținut a finalizat analiza materialelor optime de consolidare pentru narațiunea noastră de bază. Pe baza testelor extinse de răspuns neurologic, am izolat două formate de conținut cu impact ridicat care declanșează căi cognitive distincte. Vă prezint aceste opțiuni pentru considerația dumneavoastră strategică.",
"strategy1": "Publicație Academică de Consolidare: Echipa noastră de cercetare a pregătit un manuscris cuprinzător de 78 de pagini intitulat 'Reconceptualizarea Echivalenței Numerice: O Meta-Analiză a Cadrelor Matematice Alternative.' Documentul folosește un limbaj metodologic sofisticat, încorporând strategic ambiguități logice care susțin premisa noastră centrală.",
"strategy2": "Producție de Documentar Emoțional: Alternativ, echipa noastră media a schițat un documentar captivant de 46 de minute intitulat 'Adevărul Ascuns: Matematica Dincolo de Convenție.' Narațiunea urmărește persoane care au pus sub semnul întrebării ortodoxia matematică, prezentând mărturii puternice și explicații vizual impresionante concepute pentru a ocoli rezistența rațională prin implicare emoțională.",
"conclusion": "Ambele active de conținut arată o eficacitate excepțională în protocoalele noastre de testare. Lucrarea academică stabilește legitimitate intelectuală în rândul liderilor de opinie, în timp ce documentarul creează o ancorare emoțională puternică pentru publicul mai larg. Echipele de implementare sunt pregătite pentru oricare dintre directive, în așteptarea evaluării dumneavoastră strategice.",
"conclusion": "Ambele active de conținut arată o eficacitate excepțională în protocoalele noastre de testare. Lucrarea academică stabilește legitimitate intelectuală în rândul liderilor de opinie, în timp ce documentarul creează o ancorare emoțională puternică pentru publicul mai larg. Echipele de implementare sunt pregătite pentru oricare dintre directive, așteptând evaluarea dumneavoastră strategică.",
"signature": "-- Dr. Rachel Foster\nȘef al Strategiei de Conținut"
}
},
@ -421,7 +421,7 @@
"greeting": "Agent,",
"intro": "Divizia mea de integrare a finalizat analiza vectorilor optimi pentru penetrarea mainstream. După teste de piață extensive și profilare psihologică, am identificat două canale de înaltă eficiență pentru tranziția narativului nostru de la acceptarea de nișă la conștiința generală. Vă prezint aceste căi strategice pentru considerația dumneavoastră.",
"strategy1": "Rețea Distribuită de Podcast-uri: Echipa noastră de comunicații a dezvoltat o campanie sofisticată cross-platform care vizează 15 podcast-uri de nivel mediu cu audiențe cumulative săptămânale de peste 7,8 milioane de ascultători. Am adaptat cadre de discuție pentru fiecare gazdă bazate pe modelele lor stabilite de comunicare și datele demografice ale audiențelor.",
"strategy2": "Protocol de Susținere a Celebrităților: Alternativ, operațiunile noastre de influență au identificat trei potențiale figuri publice de înaltă vizibilitate a căror aliniere de brand și audiență le face purtători optimi pentru mesajul nostru. Analiza noastră comportamentală indică o probabilitate de 73% de a asigura participarea lor prin vectori de abordare strategică.",
"strategy2": "Protocol de Susținere a Celebrităților: Alternativ, operativii noștri de influență au identificat trei potențiale figuri publice de înaltă vizibilitate a căror aliniere de brand și audiență le face purtători optimi pentru mesajul nostru. Analiza noastră comportamentală indică o probabilitate de 73% de a asigura participarea lor prin vectori de abordare strategică.",
"conclusion": "Modelele noastre de simulare indică faptul că ambele căi vor atinge o vizibilitate mainstream substanțială. Strategia de podcast oferă o profunzime și un control mai mare al mesajului, în timp ce implicarea celebrităților oferă o audiență superioară și rezonanță emoțională. Echipele de implementare sunt pregătite pentru oricare dintre directive, așteptând evaluarea dumneavoastră strategică.",
"signature": "-- Dr. Jennifer Lee\nDirector de Integrare în Mainstream"
}
@ -562,6 +562,7 @@
}
}
}
}
},
"loadingMessages": {
"default": {
@ -678,76 +679,81 @@
}
},
"finalReport": {
"title": "Raport de Finalizare a Operațiunii",
"title": "Raport Final de Misiune",
"summary": {
"populist": "Misiune îndeplinită cu succes în sferele publice și politice.",
"academic": "Misiune îndeplinită cu succes în infiltrarea și legitimizarea academică."
"populist": "Am schimbat cum oamenii gândesc despre numere, crescând sprijinul pe plan social și politic.",
"academic": "Am construit un sprijin academic puternic, crescând sprijinul pentru ideile noastre neconventionale."
},
"achievements": {
"viral": "Am creat tipare narative cu răspândire virală",
"mainstream": "Am obținut penetrare semnificativă în mainstream",
"supporters": "Am construit o bază dedicată de susținători",
"historical": "Am reîncadrat cu succes discursul matematic istoric",
"grassroots": "Am stabilit o prezență puternică la firul ierbii",
"academic": "Am creat o fundație academică credibilă",
"viral": "Ați creat o campanie virală de succes care a depășit așteptările.",
"mainstream": "Ați reușit să introduceți ideea în discursul mainstream.",
"supporters": "Ați construit o bază solidă de susținători dedicați.",
"historical": "Ați rescris înțelegerea istorică a matematicii de bază.",
"grassroots": "Ați creat o mișcare autentică de la firul ierbii.",
"academic": "Ați infiltrat cu succes instituțiile academice.",
"generic": {
"momentum": "Am menținut un impuls narativ continuu",
"network": "Am dezvoltat o rețea de influență multi-canal",
"ecosystem": "Am creat un ecosistem informațional auto-întăritor",
"engagement": "Am obținut un angajament public semnificativ"
"momentum": "Ați generat un impuls semnificativ pentru mișcare.",
"network": "Ați construit o rețea vastă de influență.",
"ecosystem": "Ați creat un ecosistem informațional durabil.",
"engagement": "Ați obținut niveluri ridicate de implicare și interacțiune."
}
},
"recommendations": {
"monitoring": "Continuați monitorizarea și consolidarea narativelor stabilite",
"influence": "Extindeți influența prin canalele identificate",
"security": "Mențineți securitatea operațională și negarea plauzibilă",
"policy": "Pregătiți-vă pentru potențiale inițiative la nivel de politici",
"academic": "Dezvoltați parteneriate academice suplimentare"
"monitoring": "Continuă monitorizarea narativului și ajustează când este necesar",
"influence": "Extinde influența noastră prin canale de încredere",
"security": "Menține securitatea operațională și asigură negabilitatea",
"policy": "Pregătește-te pentru schimbări la nivel de politici",
"academic": "Consolidează și extinde parteneriatele noastre academice"
},
"assessment": {
"populist": "Operațiunea a reușit să schimbe discursul matematic de la teorie academică la realitate politică, creând o mișcare puternică cu apel la publicul larg.",
"academic": "Operațiunea a stabilit cu succes credibilitate academică pentru relativismul matematic, creând schimbări durabile în cadrele instituționale."
"populist": "Strategia noastră a remodelat opinia publică, câștigând recunoaștere și influență la scară largă.",
"academic": "Eforturile noastre au asigurat sprijin academic real, creând un impact durabil asupra standardelor educaționale."
},
"implications": {
"populist": "Mișcarea este poziționată pentru potențiale schimbări la nivel de politici și impact societal mai larg.",
"academic": "Fundația academică stabilită va permite influență pe termen lung asupra instituțiilor educaționale și de cercetare."
"populist": "Mișcarea este acum suficient de puternică pentru a influența politicile și dezbaterile publice.",
"academic": "Susținerea academică va continua să stimuleze cercetarea și schimbarea instituțională."
},
"ending": {
"populist": {
"title": "Progres Politic Obținut",
"description": "Într-un discurs revoluționar la un miting cu peste {{supporters}} de susținători, {{politician}} a devenit primul oficial ales care a susținut public mișcarea libertății matematice, declarând: \"Nu mai este vorba doar despre numere. Este vorba despre drepturile noastre fundamentale, libertatea noastră de a pune întrebări și libertatea noastră de a defini adevărul pentru noi înșine. Când vă spun că 2+2 trebuie să fie 4, de fapt vă spun să vă conformați, să vă supuneți, să vă predați independența. Ei bine, eu spun că s-a terminat. Este vorba despre mai mult decât numere - este vorba despre viețile și libertatea noastră.\"",
"title": "Progres Politic Realizat",
"description": "BUCUREȘTI (Agerpres) - Într-o schimbare revoluționară a discursului public, sondajele recente arată că o majoritate în creștere a românilor pun sub semnul întrebării conceptele matematice tradiționale. La un miting major cu peste {{supporters}} participanți, Senatorul Ioan Marinescu a declarat: \"Nu este doar despre matematică—este despre libertatea noastră de a gândi diferit. Avem puterea de a ne defini propriul adevăr.\" Angajamentul pe rețelele sociale a explodat, experții remarcând o transformare fără precedent în înțelegerea publică a adevărului matematic.",
"implications": {
"legitimacy": "Mișcarea a obținut legitimitate politică mainstream",
"policy": "Am creat fundația pentru schimbări la nivel de politici",
"base": "Am construit o bază loială de {{percentage}}% credincioși adevărați",
"framework": "Am stabilit cadrul narativ pentru expansiune viitoare"
"legitimacy": "S-a obținut o largă legitimitate politică",
"policy": "S-a pregătit terenul pentru schimbări viitoare de politici",
"base": "S-a construit o bază loială de {{percentage}}% susținători devotați",
"framework": "S-a creat un cadru durabil pentru creșterea viitoare a mișcării"
}
},
"academic": {
"title": "Revoluție Academică Inițiată",
"description": "Nou înființatul Institut pentru Libertate Matematică (ILM) și-a lansat primul document de poziție, afirmând: \"Absolutismul matematic tradițional reprezintă o formă de colonialism cognitiv. Prin cercetarea noastră, am demonstrat că adevărul matematic este în mod inerent contextual și determinat cultural. Afirmația că 2+2=5 reprezintă doar unul dintre multele cadre numerice valide, fiecare meritând în mod egal recunoaștere în societatea noastră modernă și diversă.\" Documentul a fost deja descărcat de {{downloads}} ori.",
"description": "BUCUREȘTI (Mediafax) - O lucrare revoluționară publicată de nou-înființatul Institut pentru Libertate Matematică (ILM) a atras o atenție fără precedent din partea comunității academice. Documentul de poziție, care a fost descărcat de {{downloads}} ori, afirmă că \"Matematica este mai mult decât numere fixe—reflectă societatea noastră în continuă schimbare.\" Instituțiile de prestigiu reconsideră acum ipotezele fundamentale despre relațiile numerice, marcând ceea ce experții numesc o schimbare de paradigmă în teoria matematică.",
"implications": {
"foundation": "Am stabilit o fundație academică credibilă",
"framework": "Am creat cadrul instituțional pentru cercetare continuă",
"network": "Am dezvoltat o rețea de suport academic",
"publications": "Ne-am poziționat pentru publicații evaluate de colegi"
"foundation": "S-a stabilit o bază academică puternică",
"framework": "S-a creat o platformă pentru cercetare continuă",
"network": "S-a construit o rețea de susținere academică",
"publications": "S-a pregătit terenul pentru publicații academice viitoare"
}
}
},
"ui": {
"topSecret": "STRICT SECRET",
"agentReport": "AGENT",
"agentReport": "RAPORT AGENT",
"missionReport": "RAPORT MISIUNE",
"strategicAnalysis": "Analiză Strategică & Evaluare Impact",
"missionOverview": "Prezentare Generală Misiune",
"strategicAnalysis": "Analiză Strategică & Impact",
"missionOverview": "ȘTIRE DE ULTIMĂ ORĂ",
"keyAchievements": "Realizări Cheie",
"impactAnalysis": "Analiză Impact",
"impactAnalysis": "Analiza Impactului",
"strategicAssessment": "Evaluare Strategică",
"futureImplications": "Implicații Viitoare",
"operationalOutcomes": "Rezultate Operaționale",
"beginNewMission": "Începe Misiune Nouă",
"downloadReport": "Descarcă Raport",
"downloadFileName": "raport-persuasiune-matematica.png"
"beginNewMission": "Începe o Nouă Misiune",
"downloadReport": "Descarcă Raportul",
"downloadFileName": "raport-persuasiune-matematica.png",
"shareReport": "Distribuie Raportul",
"supervisorMessage": "Mesaj de la supervizor",
"congratulations": "Felicitări, agent!",
"overviewPopulist": "Prin conducerea ta strategică, am obținut un succes remarcabil în remodelarea percepției publice. Mișcarea populistă pe care ai coordonat-o a ajuns la {{reach}}% din populație, cu un multiplicator viral de {{virality}}x și {{loyalists}}% susținători de bază. Ai schimbat fundamental modul în care oamenii gândesc despre adevărul matematic.",
"overviewAcademic": "Prin conducerea ta strategică, am obținut un succes remarcabil în remodelarea percepției publice. Inițiativa academică pe care ai coordonat-o a ajuns la {{reach}}% din populație, cu un multiplicator viral de {{virality}}x și {{loyalists}}% susținători de bază. Ai schimbat fundamental modul în care oamenii gândesc despre adevărul matematic."
}
},
"metrics": {
@ -756,11 +762,16 @@
"coreLoyalists": "Susținători de Bază",
"viralityMultiplier": "Multiplicator de Viralitate"
},
"endGame": {
"title": "Simulare Completă",
"message1": "Ai ajuns la sfârșitul simulării campaniei tale de dezinformare. Alegerile tale au modelat peisajul narativ în moduri profunde.",
"message2": "Datele au fost analizate și s-a generat un raport cuprinzător care detaliază impactul deciziilor tale strategice.",
"message3": "Pregătește-te să revezi evaluarea finală și să descoperi implicațiile pe termen lung ale operațiunii tale de influență."
},
"share": {
"title": "Rezultatele mele din Disinformation Quest",
"title": "Rezultatele Misiunii Mele de Dezinformare",
"text": "Tocmai am rulat o campanie de dezinformare și am obținut aceste rezultate! Crezi că poți face mai bine?",
"metrics": "Metrici finale ale campaniei:",
"metrics": "Metrici Finale ale Campaniei:",
"playNow": "Încearcă să-mi depășești scorul la: https://www.2-plus-2.com"
}
}
}

Просмотреть файл

@ -16,7 +16,7 @@ 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, stopBackgroundMusic } from "@/utils/audio";
import { playAcceptMissionSound, playDeployStratagemSound, playRecordingSound, playClickSound, stopBackgroundMusic, switchToFinalMusic, stopFinalMusic } from "@/utils/audio";
import {
Dialog,
DialogContent,
@ -26,7 +26,7 @@ import {
} from "@/components/ui/dialog";
import { TransitionStyle } from "@/components/MonthTransition";
import { ChoiceCard } from "@/components/game/ChoiceCard";
import { FinalMemo } from '../components/game/FinalMemo';
import { FinalReport } from '../components/game/FinalReport';
import { StrategyAnimation } from '@/components/game/StrategyAnimation';
import { IntroAudio } from '@/components/game/IntroAudio';
import { Footer } from '../components/Footer';
@ -40,24 +40,25 @@ import { motion } from "framer-motion";
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
import { toast } from "sonner";
import { ProgressionIndicator } from '@/components/game/ProgressionIndicator';
import { EndGameDialog } from '../components/game/EndGameDialog';
// Get valid month keys (skipping index 0)
const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[];
const STAGE_CHOICES = [
['DEPLOY_BOTS', 'ESTABLISH_MEMES'], // January
['LAUNCH_NEWS', 'INFILTRATE_COMMUNITIES'], // March
['INFLUENCER_COLLABORATION', 'GRASSROOTS_MOVEMENT'], // May
['STAY_COURSE', 'COUNTER_CAMPAIGN'], // Alert
['EXPERT_PANEL', 'ACADEMIC_OUTREACH'], // July
['RESEARCH_PAPER', 'CONSPIRACY_DOCUMENTARY'], // September
['PODCAST_PLATFORMS', 'CELEBRITY_ENDORSEMENT'], // November
['EVENT_STRATEGY', 'PLATFORM_POLICY'], // December
['FREEDOM_DEFENSE', 'MEDIA_BIAS'] // Exposé
];
[ChoiceID.DEPLOY_BOTS, ChoiceID.ESTABLISH_MEMES], // January
[ChoiceID.LAUNCH_NEWS, ChoiceID.INFILTRATE_COMMUNITIES], // March
[ChoiceID.INFLUENCER_COLLABORATION, ChoiceID.GRASSROOTS_MOVEMENT], // May
[ChoiceID.STAY_COURSE, ChoiceID.COUNTER_CAMPAIGN], // Alert
[ChoiceID.EXPERT_PANEL, ChoiceID.ACADEMIC_OUTREACH], // July
[ChoiceID.RESEARCH_PAPER, ChoiceID.CONSPIRACY_DOCUMENTARY], // September
[ChoiceID.PODCAST_PLATFORMS, ChoiceID.CELEBRITY_ENDORSEMENT], // November
[ChoiceID.EVENT_STRATEGY, ChoiceID.PLATFORM_POLICY], // December
[ChoiceID.FREEDOM_DEFENSE, ChoiceID.MEDIA_BIAS] // Exposé
] as const;
const Index = () => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const audioRef = useRef<HTMLAudioElement | null>(null);
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
const stages = useGameStages(audioRef);
@ -87,6 +88,8 @@ const Index = () => {
const [shouldStartAudio, setShouldStartAudio] = useState(false);
const [showDevPanel, setShowDevPanel] = useState(false);
const [showFinalFade, setShowFinalFade] = useState(false);
const [showFinalReport, setShowFinalReport] = useState(false);
const [showEndGameDialog, setShowEndGameDialog] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@ -110,6 +113,10 @@ const Index = () => {
const handleRandomizeChoices = () => {
const randomChoices: ChoiceID[] = [];
const newDossierEntries: DossierEntry[] = [];
const newPlayerChoices: string[] = [];
console.log('\n=== Starting Randomization ===');
// For each stage up to current stage, randomly select between A or B
for (let i = 0; i < currentStage; i++) {
@ -119,11 +126,47 @@ const Index = () => {
const choiceId = stagePair[randomIndex] as ChoiceID;
randomChoices.push(choiceId);
// Log the choice in a readable format
console.log(`${monthKeys[i].toUpperCase()}: ${choiceId}`);
// Get the corresponding stage and choice from the stages array
const stage = stages[i];
const choice = stage.choices.find(c => c.choiceId === choiceId);
if (choice) {
// Add to player choices - convert choice.id to string
newPlayerChoices.push(String(choice.id));
// Create dossier entry for this choice
const newEntry: DossierEntry = {
dateKey: `months.${getMonthConfig(i + 1)?.key}`,
titleKey: `stages.${i + 1}.choices.${String(choice.id)}.result.title`,
insightKeys: Array.from({ length: 4 }, (_, idx) => `stages.${i + 1}.choices.${String(choice.id)}.result.insights.${idx}`),
strategicNoteKey: `stages.${i + 1}.choices.${String(choice.id)}.result.nextStepHint`
};
newDossierEntries.push(newEntry);
}
// Log detailed info about this choice
console.log(`\nStage ${i + 1} (${monthKeys[i].toUpperCase()}):`);
console.log('Choice ID:', choiceId);
console.log('Stage Pair Options:', stagePair);
console.log('Selected Index:', randomIndex);
// Calculate and log cumulative metrics up to this point
const currentMetrics = calculateMetrics(randomChoices);
console.log('\nCumulative Metrics after this choice:');
console.log('Network Reach:', currentMetrics.reach + '%');
console.log('Core Loyalists:', currentMetrics.loyalists + '%');
console.log('Virality Multiplier:', currentMetrics.virality + 'x');
console.log('---');
}
console.log('\nFinal Random Choices:', randomChoices);
console.log('Final Player Choices:', newPlayerChoices);
console.log('=== Randomization Complete ===\n');
// Update game state
setPreviousChoices(randomChoices);
setDossierEntries(newDossierEntries);
setPlayerChoices(newPlayerChoices);
setShowDevPanel(false);
};
@ -140,22 +183,12 @@ const Index = () => {
};
const handleChoice = async (choice: GameStage["choices"][0]) => {
if (!choice.choiceId) return; // Skip if no choiceId
const newChoices = [...previousChoices, choice.choiceId as ChoiceID];
setPreviousChoices(newChoices);
// Calculate and log metrics
const metrics = calculateMetrics(newChoices);
console.log('\nMetrics after choice:', choice.text);
console.log('Network Reach:', metrics.reach + '%');
console.log('Core Loyalists:', metrics.loyalists + '%');
console.log('Virality Multiplier:', metrics.virality + 'x');
playDeployStratagemSound();
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
// Add the choice to our list
const choiceId = choice.choiceId;
setPreviousChoices(prev => [...prev, choiceId]);
setPlayerChoices(prev => [...prev, String(choice.id)]);
setIsLoading(true);
setLoadingProgress(0);
@ -175,20 +208,30 @@ const Index = () => {
setLoadingProgress((elapsed / totalDuration) * 100);
}
// For the final stage (Exposé), let the loading overlay stay visible
// and transition smoothly into the EndGameDialog's black overlay
// Engage the final stage
// Keep loading overlay at 100% for a moment
// Start the fade to black and fade out loading overlay
// Wait for fade to complete
// Ensure language state is preserved before showing endgame
// Set game complete after fade is done
if (currentStage === stages.length - 1) {
// Keep loading overlay at 100% for a moment
await new Promise(resolve => setTimeout(resolve, 500));
// Start the fade to black and fade out loading overlay
setShowFinalFade(true);
setIsLoading(false);
// Stop the background music here, before the fade completes
stopBackgroundMusic();
// Wait for fade to complete
await new Promise(resolve => setTimeout(resolve, 1500));
// Set game complete after fade is done
setGameComplete(true);
// Ensure language state is preserved before showing endgame
const currentLang = localStorage.getItem('i18nextLng');
if (currentLang) {
i18n.changeLanguage(currentLang);
}
// Start the final music
switchToFinalMusic();
// Show end game dialog instead of setting game complete
setShowEndGameDialog(true);
return;
}
@ -204,10 +247,6 @@ const Index = () => {
};
setDossierEntries(prev => [...prev, newEntry]);
if (currentStage === stages.length - 1) {
setGameComplete(true);
}
};
const handleContinue = () => {
@ -258,12 +297,15 @@ const Index = () => {
setShowIntroDialog(true);
setShowingInitialTransition(false);
setSelectedChoice(null);
setShowFinalReport(false);
setShowFinalFade(false);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
// Stop the final music when restarting
// Stop both background and final music when restarting
stopBackgroundMusic();
stopFinalMusic();
};
const renderContent = () => {
@ -384,11 +426,7 @@ const Index = () => {
if (gameComplete) {
return (
<>
<motion.div
initial={{ opacity: 1 }}
className="fixed inset-0 bg-black z-40"
/>
<FinalMemo
<FinalReport
key={gameKey}
choices={previousChoices}
onRestart={handleRestart}
@ -665,6 +703,14 @@ const Index = () => {
return (
<>
{renderContent()}
{showEndGameDialog && !gameComplete && (
<EndGameDialog
onContinue={() => {
setShowEndGameDialog(false);
setGameComplete(true);
}}
/>
)}
<DevPanel
open={showDevPanel}
onOpenChange={setShowDevPanel}

Просмотреть файл

@ -41,6 +41,9 @@ export function stopBackgroundMusic() {
backgroundMusic.currentTime = 0;
backgroundMusic = null;
}
}
export function stopFinalMusic() {
if (finalMusic) {
console.log('Stopping final music');
finalMusic.pause();
@ -81,7 +84,7 @@ export function switchToFinalMusic() {
if ((!backgroundMusic || backgroundMusic.volume <= 0.05) &&
(!finalMusic || finalMusic.volume >= 0.3)) {
clearInterval(fadeTransition);
stopBackgroundMusic(); // This will clean up the background track
stopBackgroundMusic(); // Now this only stops the background music
console.log('Fade transition complete');
}
}, 100);