578 lines
20 KiB
TypeScript
578 lines
20 KiB
TypeScript
import React, { useState, useRef, useEffect } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import {
|
|
Image,
|
|
ClueHintModal,
|
|
ClueBookModal,
|
|
flyToAnimation,
|
|
Video,
|
|
StudyGuideModal,
|
|
Cursor,
|
|
} from "@/components";
|
|
import { SuccessAlert, ErrorAlert } from "@/components/layout";
|
|
import { SingleChoiceQuizPanel } from "@/components/layout/quiz";
|
|
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
|
import { useClueStore } from "@/store/clueStore";
|
|
import type { ClueHint } from "@/components/clue";
|
|
|
|
const starName = "sunzhongshan";
|
|
const sceneId = `${starName}/reflection/scene3`;
|
|
const GUIDE_STORAGE_KEY = `${sceneId}-guide-shown`;
|
|
|
|
// 线索配置 - 根据实际图片调整位置
|
|
const clueConfigs: ClueHint[] = [
|
|
{
|
|
id: `${sceneId}/clue1`,
|
|
name: "线索1",
|
|
position: {
|
|
top: "38%",
|
|
left: "8%",
|
|
width: "24%",
|
|
height: "60%",
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue2`,
|
|
name: "线索2",
|
|
position: {
|
|
top: "26%",
|
|
left: "43%",
|
|
width: "40%",
|
|
height: "26%",
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue3`,
|
|
name: "线索3",
|
|
position: {
|
|
top: "44%",
|
|
left: "10%",
|
|
width: "85%",
|
|
height: "50%",
|
|
},
|
|
},
|
|
{
|
|
id: `${sceneId}/clue4`,
|
|
name: "线索4",
|
|
position: {
|
|
top: "75%",
|
|
left: "95.5%",
|
|
width: "0%",
|
|
height: "0%",
|
|
},
|
|
},
|
|
];
|
|
|
|
const SunzhongshanReflectionScene3 = () => {
|
|
const navigate = useNavigate();
|
|
const { moduleName } = useParams<{
|
|
moduleName: string;
|
|
}>();
|
|
|
|
const [showClueHint, setShowClueHint] = useState(false);
|
|
const [showClueBook, setShowClueBook] = useState(false);
|
|
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
|
const [successAlertContent, setSuccessAlertContent] = useState("");
|
|
const [showErrorAlert, setShowErrorAlert] = useState(false);
|
|
const [errorAlertContent, setErrorAlertContent] = useState("");
|
|
const [showVideo, setShowVideo] = useState(true);
|
|
const [showGuide, setShowGuide] = useState(false);
|
|
const [showQuiz1, setShowQuiz1] = useState(false);
|
|
const [showQuiz2, setShowQuiz2] = useState(false);
|
|
const [showQuiz3, setShowQuiz3] = useState(false);
|
|
const [showCompleteGuide, setShowCompleteGuide] = useState(false);
|
|
const [pendingQuiz, setPendingQuiz] = useState<
|
|
"quiz2" | "quiz3" | "complete" | null
|
|
>(null);
|
|
// 场景切换:true 表示 bg 场景,false 表示"窃窃私语"场景
|
|
const [isMainScene, setIsMainScene] = useState(false);
|
|
// 控制 Cursor 显示:获取线索1后显示,点击后进入主场景
|
|
const [showCursor, setShowCursor] = useState(false);
|
|
|
|
const { collectClue, isClueCollected } = useClueStore();
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const clueBookButtonRef = useRef<HTMLDivElement>(null);
|
|
// 为每个线索创建 ref
|
|
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(
|
|
new Map()
|
|
);
|
|
|
|
// 获取或创建线索的 ref
|
|
const getClueRef = (
|
|
clueId: string
|
|
): React.RefObject<HTMLDivElement | null> => {
|
|
if (!clueRefs.current.has(clueId)) {
|
|
clueRefs.current.set(clueId, React.createRef<HTMLDivElement>());
|
|
}
|
|
return clueRefs.current.get(clueId)!;
|
|
};
|
|
|
|
// 处理线索点击
|
|
const handleClueClick = (clueId: string, clueName: string) => {
|
|
// 如果已收集,不重复收集
|
|
if (isClueCollected(clueId)) {
|
|
setSuccessAlertContent("该线索已收集");
|
|
setShowSuccessAlert(true);
|
|
return;
|
|
}
|
|
|
|
// 收集线索
|
|
collectClue(clueId, clueName);
|
|
|
|
// 获取线索元素的 ref
|
|
const clueRef = getClueRef(clueId);
|
|
|
|
// 使用函数式 API 触发飞行动画
|
|
if (clueRef.current && clueBookButtonRef.current) {
|
|
flyToAnimation({
|
|
fromRef: clueRef as React.RefObject<HTMLElement>,
|
|
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
|
targetPosition: "center",
|
|
duration: 1200,
|
|
onComplete: () => {
|
|
// 动画完成后的回调
|
|
setSuccessAlertContent("获得该线索!快去线索簿查看详细信息吧!");
|
|
setShowSuccessAlert(true);
|
|
|
|
// 如果是在"窃窃私语"场景收集了线索1,显示 Cursor
|
|
if (!isMainScene && clueId === `${sceneId}/clue1`) {
|
|
setShowCursor(true);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// 处理背包点击 - 只收集线索4,不打开线索提示框
|
|
const handleBackpackClick = () => {
|
|
// 如果不在主场景,提示需要先获取线索1
|
|
if (!isMainScene) {
|
|
setErrorAlertContent(showCursor ? "该线索已收集" : "请先获取线索1");
|
|
setShowErrorAlert(true);
|
|
return;
|
|
}
|
|
|
|
const clue4Id = `${sceneId}/clue4`;
|
|
const clue4Name = "线索4";
|
|
|
|
// 如果线索4已收集,显示提示
|
|
if (isClueCollected(clue4Id)) {
|
|
setSuccessAlertContent("该线索已收集");
|
|
setShowSuccessAlert(true);
|
|
return;
|
|
}
|
|
|
|
// 收集线索4
|
|
collectClue(clue4Id, clue4Name);
|
|
|
|
// 获取线索4的 ref
|
|
const clue4Ref = getClueRef(clue4Id);
|
|
|
|
// 执行飞行动画
|
|
if (clue4Ref.current && clueBookButtonRef.current) {
|
|
flyToAnimation({
|
|
fromRef: clue4Ref as React.RefObject<HTMLElement>,
|
|
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
|
|
targetPosition: "center",
|
|
duration: 1200,
|
|
onComplete: () => {
|
|
// 动画完成后显示成功提示
|
|
setSuccessAlertContent("获得该线索!快去线索簿查看详细信息吧!");
|
|
setShowSuccessAlert(true);
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// 处理线索提示按钮点击 - 打开线索提示框
|
|
const handleClueHintClick = () => {
|
|
setShowClueHint(true);
|
|
};
|
|
|
|
// 处理视频播放结束
|
|
const handleVideoEnd = () => {
|
|
setShowVideo(false);
|
|
// 检查是否需要显示学习引导
|
|
const hasShownGuide = localStorage.getItem(GUIDE_STORAGE_KEY);
|
|
if (!hasShownGuide) {
|
|
setShowGuide(true);
|
|
}
|
|
};
|
|
|
|
// 处理学习引导关闭
|
|
const handleGuideClose = () => {
|
|
setShowGuide(false);
|
|
localStorage.setItem(GUIDE_STORAGE_KEY, "true");
|
|
};
|
|
|
|
// 检查是否所有线索都已收集
|
|
const areAllCluesCollected = () => {
|
|
return clueConfigs.every((clue) => isClueCollected(clue.id));
|
|
};
|
|
|
|
// 处理线索簿关闭
|
|
const handleClueBookClose = () => {
|
|
setShowClueBook(false);
|
|
// 如果所有线索都已收集,显示题目1
|
|
if (areAllCluesCollected()) {
|
|
setShowQuiz1(true);
|
|
}
|
|
};
|
|
|
|
// 处理题目1提交
|
|
const handleQuiz1Submit = (_value: string, isCorrect: boolean) => {
|
|
if (isCorrect) {
|
|
setShowQuiz1(false);
|
|
setSuccessAlertContent("回答正确!");
|
|
setShowSuccessAlert(true);
|
|
setPendingQuiz("quiz2");
|
|
}
|
|
};
|
|
|
|
// 处理题目2提交 - 两个选项都正确
|
|
const handleQuiz2Submit = (_value: string, _isCorrect: boolean) => {
|
|
// 题目2的两个选项都正确,所以总是成功
|
|
setShowQuiz2(false);
|
|
setSuccessAlertContent("回答正确!");
|
|
setShowSuccessAlert(true);
|
|
setPendingQuiz("quiz3");
|
|
};
|
|
|
|
// 处理题目3提交
|
|
const handleQuiz3Submit = (_value: string, isCorrect: boolean) => {
|
|
if (isCorrect) {
|
|
setShowQuiz3(false);
|
|
setSuccessAlertContent("回答正确!");
|
|
setShowSuccessAlert(true);
|
|
// 设置待显示的完成提示
|
|
setPendingQuiz("complete");
|
|
}
|
|
};
|
|
|
|
// 处理完成提示关闭
|
|
const handleCompleteGuideClose = () => {
|
|
setShowCompleteGuide(false);
|
|
};
|
|
|
|
// 处理 Cursor 点击 - 切换到主场景
|
|
const handleCursorClick = () => {
|
|
setShowCursor(false);
|
|
setIsMainScene(true);
|
|
};
|
|
|
|
// 获取线索图片
|
|
const getClueImage = (clueId: string) => {
|
|
const clueNumber = clueId.split("clue")[1];
|
|
return getStaticImage(`${sceneId}/线索${clueNumber}`);
|
|
};
|
|
|
|
// 获取线索详情图片
|
|
const getClueDetailImage = (clueId: string) => {
|
|
const clueNumber = clueId.split("clue")[1];
|
|
return getStaticImage(`${sceneId}/线索${clueNumber}-详情`);
|
|
};
|
|
|
|
// 初始化线索(如果还没有)
|
|
useEffect(() => {
|
|
clueConfigs.forEach((clue) => {
|
|
if (!isClueCollected(clue.id)) {
|
|
// 不自动收集,只是确保线索存在
|
|
}
|
|
});
|
|
}, [isClueCollected]);
|
|
|
|
// 如果线索1已收集但还在"窃窃私语"场景,显示 Cursor
|
|
useEffect(() => {
|
|
const clue1Id = `${sceneId}/clue1`;
|
|
if (!isMainScene && isClueCollected(clue1Id)) {
|
|
setShowCursor(true);
|
|
}
|
|
}, [isMainScene, isClueCollected]);
|
|
|
|
// 根据当前场景决定背景图片和显示的线索
|
|
const currentBackground = isMainScene
|
|
? getBackgroundImage(`${starName}/reflection/scene3/bg`)
|
|
: getBackgroundImage(`${starName}/reflection/scene3/窃窃私语`);
|
|
|
|
// 根据当前场景过滤线索:窃窃私语场景只显示线索1,主场景显示线索2、3、4
|
|
const visibleClues = isMainScene
|
|
? clueConfigs.filter((clue) => clue.id !== `${sceneId}/clue1`)
|
|
: clueConfigs.filter((clue) => clue.id === `${sceneId}/clue1`);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full h-full bg-cover bg-center bg-no-repeat relative"
|
|
style={{
|
|
backgroundImage: currentBackground,
|
|
}}
|
|
>
|
|
{/* 视频播放 - 覆盖在页面内容之上 */}
|
|
{showVideo && (
|
|
<div className="fixed inset-0 z-[9999] bg-black">
|
|
<Video
|
|
src={getPublicPath(`video/${starName}/sunzhongshan_11.mp4`)}
|
|
className="w-full h-full"
|
|
onEnded={handleVideoEnd}
|
|
autoPlay={true}
|
|
showSkipButton={true}
|
|
onSkip={handleVideoEnd}
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* 关闭按钮 */}
|
|
<Image
|
|
className="absolute right-0 top-0 z-10"
|
|
src={getStaticImage(`${starName}/reflection/关闭`)}
|
|
button
|
|
onClick={() => {
|
|
if (moduleName && starName) {
|
|
navigate(`/${moduleName}/${starName}/reflection`);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* Cursor - 获取线索1后显示,点击进入主场景 */}
|
|
{showCursor && !isMainScene && (
|
|
<Cursor
|
|
className="absolute left-215 bottom-135"
|
|
size={0.8}
|
|
onClick={handleCursorClick}
|
|
read={false}
|
|
/>
|
|
)}
|
|
|
|
{/* 线索点击区域 */}
|
|
{visibleClues.map((clue) => {
|
|
const isCollected = isClueCollected(clue.id);
|
|
const clueRef = getClueRef(clue.id);
|
|
return (
|
|
<div key={clue.id}>
|
|
{/* 线索元素(用于克隆和飞行) */}
|
|
<div
|
|
ref={clueRef}
|
|
className="absolute pointer-events-none opacity-0"
|
|
style={{
|
|
top: clue.position.top,
|
|
left: clue.position.left,
|
|
width: clue.position.width,
|
|
height: clue.position.height,
|
|
}}
|
|
>
|
|
<div className="relative">
|
|
<Image
|
|
src={getClueImage(clue.id)}
|
|
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
|
|
/>
|
|
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 bg-yellow-400 text-black px-2 py-1 rounded text-xs font-bold whitespace-nowrap">
|
|
{clue.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* 点击区域 */}
|
|
<div
|
|
className={`
|
|
absolute cursor-pointer transition-all duration-200
|
|
${
|
|
isCollected
|
|
? "opacity-30 hover:opacity-50"
|
|
: "opacity-0 hover:opacity-20"
|
|
}
|
|
hover:bg-yellow-400/20
|
|
`}
|
|
style={{
|
|
top: clue.position.top,
|
|
left: clue.position.left,
|
|
width: clue.position.width,
|
|
height: clue.position.height,
|
|
}}
|
|
onClick={() => handleClueClick(clue.id, clue.name)}
|
|
title={
|
|
isCollected ? `${clue.name}(已收集)` : `点击收集${clue.name}`
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* 右下角-功能处 */}
|
|
<div className="absolute right-6 bottom-12 flex flex-col items-center gap-4 z-10">
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/线索提示`)}
|
|
button
|
|
onClick={handleClueHintClick}
|
|
/>
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/背包`)}
|
|
button
|
|
onClick={handleBackpackClick}
|
|
/>
|
|
<div
|
|
ref={clueBookButtonRef}
|
|
className="relative"
|
|
data-clue-book-button="true"
|
|
>
|
|
<Image
|
|
src={getStaticImage(`${starName}/reflection/线索簿`)}
|
|
button
|
|
onClick={() => setShowClueBook(true)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 线索提示窗 */}
|
|
<ClueHintModal
|
|
visible={showClueHint}
|
|
backgroundImage={getStaticImage(
|
|
isMainScene
|
|
? `${starName}/reflection/scene3/bg`
|
|
: `${starName}/reflection/scene3/窃窃私语`
|
|
)}
|
|
clues={visibleClues}
|
|
onClose={() => setShowClueHint(false)}
|
|
sourceContainerRef={containerRef}
|
|
/>
|
|
|
|
{/* 线索簿窗口 */}
|
|
<ClueBookModal
|
|
visible={showClueBook}
|
|
clues={clueConfigs.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
collected: isClueCollected(c.id),
|
|
}))}
|
|
onClose={handleClueBookClose}
|
|
getClueImage={getClueImage}
|
|
getClueDetailImage={getClueDetailImage}
|
|
/>
|
|
|
|
{/* 成功提示弹窗 */}
|
|
<SuccessAlert
|
|
visible={showSuccessAlert}
|
|
title="恭喜你!"
|
|
content={successAlertContent}
|
|
onContinue={() => {
|
|
setShowSuccessAlert(false);
|
|
setSuccessAlertContent("");
|
|
// 检查是否有待显示的题目或完成提示
|
|
if (pendingQuiz === "quiz2") {
|
|
setShowQuiz2(true);
|
|
setPendingQuiz(null);
|
|
} else if (pendingQuiz === "quiz3") {
|
|
setShowQuiz3(true);
|
|
setPendingQuiz(null);
|
|
} else if (pendingQuiz === "complete") {
|
|
setShowCompleteGuide(true);
|
|
setPendingQuiz(null);
|
|
}
|
|
}}
|
|
onClose={() => {
|
|
setShowSuccessAlert(false);
|
|
setSuccessAlertContent("");
|
|
// 检查是否有待显示的题目或完成提示
|
|
if (pendingQuiz === "quiz2") {
|
|
setShowQuiz2(true);
|
|
setPendingQuiz(null);
|
|
} else if (pendingQuiz === "quiz3") {
|
|
setShowQuiz3(true);
|
|
setPendingQuiz(null);
|
|
} else if (pendingQuiz === "complete") {
|
|
setShowCompleteGuide(true);
|
|
setPendingQuiz(null);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* 错误提示弹窗 */}
|
|
<ErrorAlert
|
|
visible={showErrorAlert}
|
|
text={errorAlertContent}
|
|
onClose={() => setShowErrorAlert(false)}
|
|
zIndex={10001}
|
|
onMaskClick={() => setShowErrorAlert(false)}
|
|
/>
|
|
|
|
{/* 学习引导弹窗 */}
|
|
<StudyGuideModal
|
|
visible={showGuide}
|
|
onContinue={handleGuideClose}
|
|
zIndex={10000}
|
|
>
|
|
后来,学校建立了中国社会主义青年团团支部,你积极靠拢,成为一名光荣的团员。
|
|
国共合作大背景下,国民革命正在如火如荼地开展,北伐也在积极酝酿之中。你在自己的岗位上认真工作,也密切关注着时局和两党动态。
|
|
{"\n"}
|
|
本游戏需要在场景中找出4条线索,并回答正确所有问题后才能完成本游戏探索。
|
|
</StudyGuideModal>
|
|
|
|
{/* 题目1:冯玉祥发动"北京政变"后邀请孙中山北上共商国是,各界对此事的看法如何? */}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz1}
|
|
title='冯玉祥发动"北京政变"后邀请孙中山北上共商国是,各界对此事的看法如何?'
|
|
options={[
|
|
{ value: "A", label: "A: 意见高度一致,认为和平统一已经十拿九稳" },
|
|
{ value: "B", label: "B: 期待、信任为主,亦有担忧和谨慎的探讨" },
|
|
]}
|
|
correctAnswer="B"
|
|
errorMessage="回答错误,请你再仔细思考一下吧。"
|
|
onSubmit={handleQuiz1Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 题目2:今天的日记写什么呢? */}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz2}
|
|
title="今天的日记写什么呢?"
|
|
options={[
|
|
{
|
|
value: "A",
|
|
label:
|
|
"A: 当前情形,反对军阀势力进而反对列强压迫已成为国共两党乃至各界共识。只是这一目标应当如何达成呢?今年上半年就听说黄埔军校在全国招生的消息,大批青年踊跃报考。可见组建军队,武力北伐是一个公认的选项。然下半年直系军阀倒台,冯玉祥电邀孙先生北上主政,便又打开了和平统一的一线契机,如能实现,亦是应了国人的期盼。今日孙先生行程抵沪,马不停蹄接见各界人士,宣讲北上主张,我辈见之无不钦佩其鞠躬尽瘁之心。前路未明,探索不止,希望国民共同努力,早日迎来和平民主的新天地。",
|
|
},
|
|
{
|
|
value: "B",
|
|
label:
|
|
'B: 孙中山先生抵沪,来自百年后的我站在欢迎的人群中,终于亲眼见到了这位伟大的革命先行者。1924年的秋天,冯玉祥北京政变为国家的和平统一带来一线契机。回望历史,我们知道,这在中国近代的民族民主革命路途上,不过是一个插曲。但对于孙中山先生而言,抱病北上,不遗余力地宣传反帝反封建主张,几乎成为他为国家前途命运而作的最后争取。他在迷途中坚守,在变局中担当,穷极一生从未停下脚步。伟大复兴的新征程上,我们早已摆脱了屈辱与苦难,向着国家富强的光明前景、人民幸福的美好未来进发。先生"振兴中华"的呐喊犹在耳畔,"天下为公"的初心始终滚烫。今天我们所能做的,便是铭记这份坚守与担当,做跨越百年的追梦路上最坚定的继承者与前行者。',
|
|
},
|
|
]}
|
|
correctAnswer="A"
|
|
errorMessage=""
|
|
onSubmit={handleQuiz2Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 题目3:毅然北上赴会的孙中山先生,为争取国家和平统一作出了哪些努力? */}
|
|
<SingleChoiceQuizPanel
|
|
visible={showQuiz3}
|
|
title="毅然北上赴会的孙中山先生,为争取国家和平统一作出了哪些努力?"
|
|
options={[
|
|
{
|
|
value: "A",
|
|
label:
|
|
"A: 重视舆论导向,动身前发表《北上宣言》明确态度,沿途不辞劳苦与各界会晤交流,反复重申主张。",
|
|
},
|
|
{
|
|
value: "B",
|
|
label: "B: 珍惜契机,沿途不做停顿,以最快的速度赶往北京。",
|
|
},
|
|
]}
|
|
correctAnswer="A"
|
|
errorMessage="回答错误,请你再仔细思考一下吧。"
|
|
onSubmit={handleQuiz3Submit}
|
|
zIndex={10001}
|
|
/>
|
|
|
|
{/* 完成提示弹窗 */}
|
|
<StudyGuideModal
|
|
visible={showCompleteGuide}
|
|
onContinue={handleCompleteGuideClose}
|
|
zIndex={10002}
|
|
>
|
|
辛亥革命拉开了中国完全意义上的近代民族民主革命的序幕。北洋军阀首领袁世凯窃取革命成果后,各地军阀势力对内割据混战,对外出卖国家利益,在政治上谋求专制和独裁,俨然成为民族独立、民主政治、民生幸福之路上的头号绊脚石。
|
|
为了改变这一时期的困局,孙中山尝试过高举护法旗帜,两度领导护法运动;与新生的中国共产党展开合作,与无产阶级工农群众共建起统一战线;为了团结一切可以团结的力量,调动一切可以调动的因素,在生命最后的日子里抱病北上,一路不遗余力地向社会各界宣传反帝反封建主张。他经历了太多挫折,却始终以“行”求“知”。
|
|
</StudyGuideModal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SunzhongshanReflectionScene3;
|