зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-29 12:46:03 +02:00
new intro screen and sounds
Этот коммит содержится в:
родитель
f9f1732b78
Коммит
6a0978eeca
Двоичные данные
public/audio/accept-mission-click.mp3
Обычный файл
Двоичные данные
public/audio/accept-mission-click.mp3
Обычный файл
Двоичный файл не отображается.
Двоичные данные
public/audio/click1.mp3
Двоичные данные
public/audio/click1.mp3
Двоичный файл не отображается.
@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
@ -19,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-50 bg-black/80 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",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -12,7 +12,14 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { AlertCircle, Lock, Shield } from "lucide-react";
|
import { AlertCircle, Lock, Shield } from "lucide-react";
|
||||||
import { Volume2, VolumeX, Volume1 } from "lucide-react";
|
import { Volume2, VolumeX, Volume1 } from "lucide-react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { playClickSound } from "@/utils/audio";
|
import { playAcceptMissionSound, playDeployStratagemSound, playRecordingSound, playClickSound } from "@/utils/audio";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface GameStage {
|
interface GameStage {
|
||||||
id: number;
|
id: number;
|
||||||
@ -455,23 +462,42 @@ const stages: GameStage[] = [
|
|||||||
|
|
||||||
const TypewriterText = ({ text, onComplete }: { text: string, onComplete?: () => void }) => {
|
const TypewriterText = ({ text, onComplete }: { text: string, onComplete?: () => void }) => {
|
||||||
const [displayedText, setDisplayedText] = useState('');
|
const [displayedText, setDisplayedText] = useState('');
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let index = 0;
|
// Reset displayed text when text prop changes
|
||||||
const timer = setInterval(() => {
|
setDisplayedText('');
|
||||||
if (index < text.length) {
|
|
||||||
setDisplayedText((prev) => prev + text[index]);
|
const characters = text.split('');
|
||||||
index++;
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
// Clear any existing interval
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
if (currentIndex < characters.length) {
|
||||||
|
setDisplayedText(prev => prev + characters[currentIndex]);
|
||||||
|
currentIndex++;
|
||||||
} else {
|
} else {
|
||||||
clearInterval(timer);
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
onComplete?.();
|
onComplete?.();
|
||||||
}
|
}
|
||||||
}, 30);
|
}, 30);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [text, onComplete]);
|
}, [text, onComplete]);
|
||||||
|
|
||||||
return <span>{displayedText}</span>;
|
// Only render the text, nothing else
|
||||||
|
return displayedText;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BriefingAudio = ({
|
const BriefingAudio = ({
|
||||||
@ -523,7 +549,7 @@ const BriefingAudio = ({
|
|||||||
if (!expertAudio) return null;
|
if (!expertAudio) return null;
|
||||||
|
|
||||||
const togglePlay = () => {
|
const togglePlay = () => {
|
||||||
playClickSound();
|
playRecordingSound();
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
audioRef.current.pause();
|
audioRef.current.pause();
|
||||||
@ -535,7 +561,7 @@ const BriefingAudio = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleMute = () => {
|
const toggleMute = () => {
|
||||||
playClickSound();
|
playRecordingSound();
|
||||||
if (isMuted) {
|
if (isMuted) {
|
||||||
setVolume(prevVolume.current);
|
setVolume(prevVolume.current);
|
||||||
setIsMuted(false);
|
setIsMuted(false);
|
||||||
@ -624,9 +650,10 @@ const Index = () => {
|
|||||||
const [loadingProgress, setLoadingProgress] = useState(0);
|
const [loadingProgress, setLoadingProgress] = useState(0);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const [showingInitialTransition, setShowingInitialTransition] = useState(false);
|
const [showingInitialTransition, setShowingInitialTransition] = useState(false);
|
||||||
|
const [showIntroDialog, setShowIntroDialog] = useState(true);
|
||||||
|
|
||||||
const handleStartGame = () => {
|
const handleStartGame = () => {
|
||||||
playClickSound();
|
playAcceptMissionSound();
|
||||||
setShowingInitialTransition(true);
|
setShowingInitialTransition(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -640,7 +667,7 @@ const Index = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChoice = async (choice: GameStage["choices"][0]) => {
|
const handleChoice = async (choice: GameStage["choices"][0]) => {
|
||||||
playClickSound();
|
playDeployStratagemSound();
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
audioRef.current.pause();
|
audioRef.current.pause();
|
||||||
audioRef.current = null;
|
audioRef.current = null;
|
||||||
@ -687,8 +714,17 @@ const Index = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
playClickSound();
|
playDeployStratagemSound();
|
||||||
setShowingResult(false);
|
setShowingResult(false);
|
||||||
|
|
||||||
|
// Check if this was the last stage
|
||||||
|
if (currentStage >= stages.length - 1) {
|
||||||
|
// Move to completion screen
|
||||||
|
setCurrentStage(stages.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, continue to next stage
|
||||||
setShowingMonthTransition(true);
|
setShowingMonthTransition(true);
|
||||||
setNextStage(currentStage + 1);
|
setNextStage(currentStage + 1);
|
||||||
};
|
};
|
||||||
@ -707,7 +743,6 @@ const Index = () => {
|
|||||||
<Button
|
<Button
|
||||||
className="fixed top-4 right-4 bg-yellow-500 hover:bg-yellow-600 text-black"
|
className="fixed top-4 right-4 bg-yellow-500 hover:bg-yellow-600 text-black"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => playClickSound()}
|
|
||||||
>
|
>
|
||||||
<ClipboardList className="w-4 h-4 mr-2" />
|
<ClipboardList className="w-4 h-4 mr-2" />
|
||||||
Dossier
|
Dossier
|
||||||
@ -771,6 +806,83 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IntroDialog = () => (
|
||||||
|
<Dialog open={showIntroDialog} onOpenChange={setShowIntroDialog}>
|
||||||
|
<DialogContent className="bg-gray-900/95 text-white border-gray-700 max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl text-yellow-500 mb-4">
|
||||||
|
Understanding Disinformation Through Simulation
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="space-y-4 text-gray-300">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-semibold text-yellow-500">What is Disinformation?</p>
|
||||||
|
<p>
|
||||||
|
Disinformation is deliberately created false information intended to mislead, harm, or
|
||||||
|
manipulate people, social groups, organizations, or countries. It's a sophisticated form
|
||||||
|
of deception that exploits existing divisions and vulnerabilities in society.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-semibold text-yellow-500">Why Does It Matter Today?</p>
|
||||||
|
<p>
|
||||||
|
In our hyperconnected world, information spreads at unprecedented speeds across global networks.
|
||||||
|
While this connectivity brings many benefits, it also creates perfect conditions for coordinated
|
||||||
|
disinformation campaigns to operate at massive scale.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We're bombarded with more information daily than we could ever hope to fact-check or verify.
|
||||||
|
This information overload, combined with sophisticated manipulation techniques, makes us all
|
||||||
|
vulnerable. The best defense is understanding how these campaigns work - building an "immune
|
||||||
|
system" against manipulation by learning to recognize their strategies.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-semibold text-yellow-500">About This Simulation</p>
|
||||||
|
<p>
|
||||||
|
In this interactive experience, you'll step into the role of a disinformation agent
|
||||||
|
with an absurd mission: convincing people that 2+2=5. While the scenario is
|
||||||
|
intentionally ridiculous, the techniques and strategies you'll encounter are based on
|
||||||
|
real-world disinformation tactics.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-semibold text-yellow-500">Educational Purpose</p>
|
||||||
|
<p>
|
||||||
|
By experiencing how disinformation campaigns operate from the inside, you'll:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 ml-4">
|
||||||
|
<li>Learn to recognize common disinformation tactics</li>
|
||||||
|
<li>Understand how false narratives spread through different channels</li>
|
||||||
|
<li>Develop better critical thinking skills to identify manipulation attempts</li>
|
||||||
|
<li>See how social psychology and cognitive biases are exploited</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-yellow-500/10 rounded-lg border border-yellow-500/20">
|
||||||
|
<p className="text-yellow-500 font-semibold">Remember:</p>
|
||||||
|
<p className="text-gray-300">
|
||||||
|
This is an educational tool designed to help you understand and combat disinformation
|
||||||
|
in the real world. The better you understand how these campaigns work, the better
|
||||||
|
equipped you'll be to recognize and resist them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex justify-center mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowIntroDialog(false)}
|
||||||
|
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-2"
|
||||||
|
>
|
||||||
|
Begin Experience
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
|
||||||
if (!gameStarted) {
|
if (!gameStarted) {
|
||||||
if (showingInitialTransition) {
|
if (showingInitialTransition) {
|
||||||
return (
|
return (
|
||||||
@ -779,12 +891,6 @@ const Index = () => {
|
|||||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center">
|
<div className="relative min-h-screen bg-transparent p-4 flex items-center">
|
||||||
<DossierPanel entries={dossierEntries} />
|
<DossierPanel entries={dossierEntries} />
|
||||||
<div className="max-w-4xl mx-auto w-full relative">
|
<div className="max-w-4xl mx-auto w-full relative">
|
||||||
<BriefingAudio
|
|
||||||
stage={stages[0].title}
|
|
||||||
audioRef={audioRef}
|
|
||||||
autoPlay={false}
|
|
||||||
className="absolute top-4 left-4"
|
|
||||||
/>
|
|
||||||
<MonthTransition
|
<MonthTransition
|
||||||
month={stages[0].title.split(":")[0]}
|
month={stages[0].title.split(":")[0]}
|
||||||
onComplete={handleInitialTransitionComplete}
|
onComplete={handleInitialTransitionComplete}
|
||||||
@ -799,6 +905,7 @@ const Index = () => {
|
|||||||
<div className="relative min-h-screen overflow-hidden">
|
<div className="relative min-h-screen overflow-hidden">
|
||||||
<GameBackground />
|
<GameBackground />
|
||||||
<div className="relative min-h-screen bg-transparent flex items-center justify-center p-4">
|
<div className="relative min-h-screen bg-transparent flex items-center justify-center p-4">
|
||||||
|
<IntroDialog />
|
||||||
<DossierPanel entries={dossierEntries} />
|
<DossierPanel entries={dossierEntries} />
|
||||||
<Card className="w-full max-w-2xl bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in backdrop-blur-sm">
|
<Card className="w-full 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">
|
<CardHeader className="text-center space-y-4">
|
||||||
|
|||||||
@ -1,37 +1,35 @@
|
|||||||
const CLICK_SOUNDS = [
|
const audioCache: { [key: string]: HTMLAudioElement } = {};
|
||||||
"/audio/click1.mp3",
|
|
||||||
"/audio/click2.mp3"
|
|
||||||
];
|
|
||||||
|
|
||||||
class AudioPlayer {
|
function createAudio(src: string): HTMLAudioElement {
|
||||||
private static instance: AudioPlayer;
|
if (audioCache[src]) {
|
||||||
private audioElements: { [key: string]: HTMLAudioElement } = {};
|
return audioCache[src];
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
// Pre-load click sounds
|
|
||||||
CLICK_SOUNDS.forEach((sound, index) => {
|
|
||||||
const audio = new Audio(sound);
|
|
||||||
audio.volume = 0.3; // Lower volume for UI sounds
|
|
||||||
this.audioElements[`click${index + 1}`] = audio;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): AudioPlayer {
|
|
||||||
if (!AudioPlayer.instance) {
|
|
||||||
AudioPlayer.instance = new AudioPlayer();
|
|
||||||
}
|
|
||||||
return AudioPlayer.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public playClickSound() {
|
|
||||||
const randomIndex = Math.floor(Math.random() * CLICK_SOUNDS.length);
|
|
||||||
const sound = this.audioElements[`click${randomIndex + 1}`];
|
|
||||||
if (sound) {
|
|
||||||
// Create a clone to allow overlapping sounds
|
|
||||||
const clone = sound.cloneNode() as HTMLAudioElement;
|
|
||||||
clone.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const audio = new Audio(src);
|
||||||
|
audioCache[src] = audio;
|
||||||
|
return audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const playClickSound = () => AudioPlayer.getInstance().playClickSound();
|
export function playSound(src: string, volume = 0.75) {
|
||||||
|
const audio = createAudio(src);
|
||||||
|
audio.volume = volume;
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play().catch(err => console.error('Audio playback failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedicated sound functions
|
||||||
|
export function playAcceptMissionSound() {
|
||||||
|
playSound('/audio/accept-mission-click.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playDeployStratagemSound() {
|
||||||
|
playSound('/audio/deploy-stratagem-click.wav');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playRecordingSound() {
|
||||||
|
playSound('/audio/play-recording-click.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic click sound (keep existing functionality)
|
||||||
|
export function playClickSound() {
|
||||||
|
playDeployStratagemSound();
|
||||||
|
}
|
||||||
Загрузка…
x
Ссылка в новой задаче
Block a user