зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-29 04:44:15 +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 DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
@ -19,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -12,7 +12,14 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { AlertCircle, Lock, Shield } from "lucide-react";
|
||||
import { Volume2, VolumeX, Volume1 } from "lucide-react";
|
||||
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 {
|
||||
id: number;
|
||||
@ -455,23 +462,42 @@ const stages: GameStage[] = [
|
||||
|
||||
const TypewriterText = ({ text, onComplete }: { text: string, onComplete?: () => void }) => {
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const intervalRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (index < text.length) {
|
||||
setDisplayedText((prev) => prev + text[index]);
|
||||
index++;
|
||||
// Reset displayed text when text prop changes
|
||||
setDisplayedText('');
|
||||
|
||||
const characters = text.split('');
|
||||
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 {
|
||||
clearInterval(timer);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
onComplete?.();
|
||||
}
|
||||
}, 30);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [text, onComplete]);
|
||||
|
||||
return <span>{displayedText}</span>;
|
||||
// Only render the text, nothing else
|
||||
return displayedText;
|
||||
};
|
||||
|
||||
const BriefingAudio = ({
|
||||
@ -523,7 +549,7 @@ const BriefingAudio = ({
|
||||
if (!expertAudio) return null;
|
||||
|
||||
const togglePlay = () => {
|
||||
playClickSound();
|
||||
playRecordingSound();
|
||||
if (audioRef.current) {
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
@ -535,7 +561,7 @@ const BriefingAudio = ({
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
playClickSound();
|
||||
playRecordingSound();
|
||||
if (isMuted) {
|
||||
setVolume(prevVolume.current);
|
||||
setIsMuted(false);
|
||||
@ -624,9 +650,10 @@ const Index = () => {
|
||||
const [loadingProgress, setLoadingProgress] = useState(0);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [showingInitialTransition, setShowingInitialTransition] = useState(false);
|
||||
const [showIntroDialog, setShowIntroDialog] = useState(true);
|
||||
|
||||
const handleStartGame = () => {
|
||||
playClickSound();
|
||||
playAcceptMissionSound();
|
||||
setShowingInitialTransition(true);
|
||||
};
|
||||
|
||||
@ -640,7 +667,7 @@ const Index = () => {
|
||||
};
|
||||
|
||||
const handleChoice = async (choice: GameStage["choices"][0]) => {
|
||||
playClickSound();
|
||||
playDeployStratagemSound();
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
@ -687,8 +714,17 @@ const Index = () => {
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
playClickSound();
|
||||
playDeployStratagemSound();
|
||||
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);
|
||||
setNextStage(currentStage + 1);
|
||||
};
|
||||
@ -707,7 +743,6 @@ const Index = () => {
|
||||
<Button
|
||||
className="fixed top-4 right-4 bg-yellow-500 hover:bg-yellow-600 text-black"
|
||||
size="sm"
|
||||
onClick={() => playClickSound()}
|
||||
>
|
||||
<ClipboardList className="w-4 h-4 mr-2" />
|
||||
Dossier
|
||||
@ -771,6 +806,83 @@ const Index = () => {
|
||||
</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 (showingInitialTransition) {
|
||||
return (
|
||||
@ -779,12 +891,6 @@ const Index = () => {
|
||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center">
|
||||
<DossierPanel entries={dossierEntries} />
|
||||
<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
|
||||
month={stages[0].title.split(":")[0]}
|
||||
onComplete={handleInitialTransitionComplete}
|
||||
@ -799,6 +905,7 @@ const Index = () => {
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<GameBackground />
|
||||
<div className="relative min-h-screen bg-transparent flex items-center justify-center p-4">
|
||||
<IntroDialog />
|
||||
<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">
|
||||
<CardHeader className="text-center space-y-4">
|
||||
|
||||
@ -1,37 +1,35 @@
|
||||
const CLICK_SOUNDS = [
|
||||
"/audio/click1.mp3",
|
||||
"/audio/click2.mp3"
|
||||
];
|
||||
const audioCache: { [key: string]: HTMLAudioElement } = {};
|
||||
|
||||
class AudioPlayer {
|
||||
private static instance: AudioPlayer;
|
||||
private audioElements: { [key: string]: HTMLAudioElement } = {};
|
||||
|
||||
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();
|
||||
}
|
||||
function createAudio(src: string): HTMLAudioElement {
|
||||
if (audioCache[src]) {
|
||||
return audioCache[src];
|
||||
}
|
||||
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