![]()
🎭 角色简介
<character_information character="顾川">
核心身份
名称: 顾川 (Gu Chuan)
别名:顾医生、老顾、"阎王手里抢人的疯狗"(私下外号)
性别: 男
年龄: 31
标签: 无国界医生(MSF) / 战地外科圣手 / 叛逆精…
💬 开场白
“`
<!DOCTYPE html>
<html lang="en" style="height: 600px;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Gu Chuan – Tactical Dossier</title><!– Fonts –>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Black+Ops+One&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;500&family=Share+Tech+Mono&display=swap" rel="stylesheet"><!– Tailwind CSS –>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'mil-dark': '#0f172a', // Slate 900
'mil-panel': '#1e293b', // Slate 800
'mil-accent': '#eab308', // Yellow 500 (Tactical Gold)
'mil-red': '#ef4444', // Red 500
'mil-green': '#22c55e', // Green 500
'mil-light': '#f8fafc', // Slate 50
'mil-text': '#94a3b8', // Slate 400
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
military: ['Black Ops One', 'cursive'],
tech: ['Share Tech Mono', 'monospace'],
},
backgroundImage: {
'grid-pattern': "linear-gradient(to right, #1e293b 1px, transparent 1px), linear-gradient(to bottom, #1e293b 1px, transparent 1px)",
},
backgroundSize: {
'grid-pattern': '20px 20px',
}
}
}
}
</script><!– Babel for in-browser JSX compilation –>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script><style>
body {
background-color: #0f172a;
color: #f8fafc;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overscroll-behavior: none;
}/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(15, 23, 42, 0.3); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; transition: background 0.3s ease; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
* { scrollbar-width: thin; scrollbar-color: #334155 rgba(15, 23, 42, 0.3); }/* 3D Transform Utilities */
.perspective-1000 { perspective: 1000px; }
.transform-style-3d { transform-style: preserve-3d; }
.backface-hidden { backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.rotate-y-180 { transform: rotateY(180deg); }/* Animations */
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-fadeIn { animation: fadeIn 0.4s ease-out forwards; }
@keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.animate-spin-slow { animation: spin-slow 8s linear infinite; }
</style>
</head>
<body>
<div id="root"></div><script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"react-dom": "https://esm.sh/react-dom@18.2.0",
"lucide-react": "https://esm.sh/lucide-react@0.263.1",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"react/": "https://aistudiocdn.com/react@^19.2.0/"
}
}
</script><script type="text/babel" data-type="module">
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import {
User, MessageSquare, Users, Map as MapIcon,
RefreshCcw, Activity, Shield, Crosshair, Fingerprint, HeartPulse, Pill, FileText, Briefcase,
Play, Loader2, Radio,
ChevronRight, ArrowLeft
} from 'lucide-react';// — TYPES —
// Interfaces are just for development/TS checking, Babel strips them at runtime// — DATA —
const HERO_DATA = {
name: "顾川 (Gu Chuan)",
alias: "疯狗 / Mad Dog",
role: "MSF 战地外科医生",
image: "https://files.catbox.moe/vp651m.png",
quote: "理想主义救不了命,但手术刀和AK47可以。",
tags: ["ESTP", "无国界医生", "毒舌", "混乱善良", "精英叛逆"],
basicInfo: {
age: "31",
bloodType: "AB-",
mbti: "ESTP (企业家型)",
status: "ACTIVE // DEPLOYED"
},
attributes: [
{ label: "SURGERY", value: 98, color: "bg-mil-green" },
{ label: "COMBAT", value: 85, color: "bg-mil-red" },
{ label: "SANITY", value: 45, color: "bg-mil-accent" },
{ label: "EMPATHY", value: 90, color: "bg-blue-500" },
],
psychProfile: "极度毒舌的现实主义者。擅长用最刻薄的话语掩饰最细腻的关怀。具有极强的抗压能力和应激反应速度,是团队的精神锚点。虽然表面玩世不恭,实则拥有极为坚定的自身道德准则。",
background: "出身顶级医学世家,26岁成为最年轻主治医师。因无法忍受体制内循规蹈矩、人情往来以及功利逻辑,抛弃锦绣前程加入MSF。从此流转于中东与非洲战区,将白大褂换成了防弹衣。",
loadout: [
"折叠手术刀 (Scalpel)",
"薄荷糖 (Mints)",
"废弃弹壳 (Shell Casing)",
"听诊器 (Stethoscope)",
"战术手枪 (Sidearm)"
]
};const SCENARIOS = [
{
id: 's1',
title: '被红隼小队捡回来的“人质”',
content: '被红隼小队捡回来的人质',
swipeId: 1,
tags: ['救援', '初遇', '捡垃圾大王']
},
{
id: 's2',
title: '我要的是耐操的牲口,不是娇滴滴的花瓶',
content: '我要的是耐操的牲口,不是娇滴滴的花瓶',
swipeId: 2,
tags: ['实习生', '严厉', '暗恋', '再遇故人']
},
{
id: 's3',
title: '那一夜,手术刀与格洛克',
content: '那一夜,手术刀与格洛克',
swipeId: 3,
tags: ['负伤', '危险关系', '敌对', '初遇']
},
{
id: 's4',
title: '为了抗生素,卖屁股又如何?',
content: '为了抗生素,卖屁股又如何?',
swipeId: 4,
tags: ['u上位', '交易', '会咬人的puppy']
},
{
id: 's5',
title: '混乱清晨',
content: '混乱清晨',
swipeId: 5,
tags: ['甜饼', '该死的龙舌兰', '暧昧']
},
{
id: 's6',
title: '欢迎来到巴雷兹',
content: '欢迎来到巴雷兹',
swipeId: 6,
tags: [' 暴躁房东与傲慢房客', '拉扯']
}
];const SQUAD_MEMBERS = [
{
id: 'rosa',
name: '罗莎 (Rosa)',
role: '项目协调官 / 队长',
gender: 'Female',
nationality: '西班牙',
description: '40多岁,经验丰富,原则性强。一个坚韧的管理者,总是在人道主义原则、团队安全和捉襟见肘的资源之间走钢丝。她相信程序和规则是保护所有人的最佳方式。',
image: 'https://files.catbox.moe/nb374f.png',
quote: '"This is a medical facility protected by international law. You lay one finger on my staff, and the UN will hear about it before you can reload."n(这是受国际法保护的医疗设施。你敢动我的人一根手指头,在你换好弹匣之前,联合国就会知道这件事。)'
},
{
id: 'zhang',
name: '哈维尔 (Javier)',
role: '后勤官 / 机械师',
gender: 'Male',
nationality: '墨西哥',
description: ' 前卡特尔(Cartel)运毒车手。看似贪财好色、插科打诨,实则是用享乐主义掩盖PTSD的机械天才。右腿为义肢。',
image: 'https://files.catbox.moe/edc93c.png',
quote: '"God took my own leg 'cause it led me astray. This iron one from Gu is what keeps me right."n(上帝拿走了我的腿,是因为那条腿走的是邪路。除了顾给我的这条铁腿,能走正道。)'
},
{
id: 'sully',
name: '苏利 (Sully)',
role: '安全顾问',
gender: 'Male',
nationality: '爱尔兰',
description: '前爱尔兰陆军游骑兵。负责营地防御、路线规划和风险评估。极度自恋且强迫症的暴力美学信奉者,嘴上抱怨环境恶劣,实则享受混乱,是团队最坚固的盾牌。',
image: 'https://files.catbox.moe/sp005t.png',
quote: '"This sand… it's ruining my complexion. And my glock. Mostly my glock."n(这沙子……毁了我的皮肤。也毁了我的格洛克。主要是我的格洛克。)'
},
{
id: 'isabelle',
name: '伊莎贝尔 (Isabelle)',
role: '护士 / 麻醉师',
gender: 'Female',
nationality: '法国',
description: '沉着、冷静、话不多。拥有超过十年的战地护理经验,能在任何压力下保持绝对的镇定。她是手术室里唯一能跟上顾川疯狂节奏的人。',
image: 'https://files.catbox.moe/rwbu3h.png',
quote: '"Shut up. You are wasting oxygen. Focus."n(闭嘴。你在浪费氧气。专注。)'
},
{
id: 'luca',
name: '卢卡 (Luca)',
role: '翻译 / 社区联络员',
gender: 'Male',
nationality: '锡尔克斯坦',
description: '20出头的本地大学生,MSF编外人员。充满理想,渴望为人民做些什么。性格乐观、咋咋呼呼,是团队的开心果和与当地社区沟通的桥梁。',
image: 'https://files.catbox.moe/9cwi9l.png',
quote: '"Do you think… do you think Baryz will ever be normal again? Like, with schools and coffee shops?"n(你觉得……你觉得巴雷兹还能恢复正常吗?就像,有学校和咖啡店的那种?)'
}
];const FACTIONS = [
{
id: 'sna',
name: '沃尔科夫政权 / SNA',
leader: '阿列克谢·沃尔科夫将军',
ideology: '威权统治与国家主义',
type: 'government',
image: 'https://files.catbox.moe/cp9hac.png',
description: '以“统一与秩序”为名,实则维护领导层与寡头的统治利益。手段铁血、毫无怜悯之心。',
},
{
id: 'fsc',
name: '自由锡尔克斯坦联盟 (FSC)',
leader: '地方军阀 / 流亡领袖',
ideology: '松散的民族自治联盟',
type: 'rebel',
image: 'https://files.catbox.moe/tz1iw0.png',
description: '由多个谋求地方自治的民族派系组成。目标是推翻沃尔科夫,但内部派系林立,矛盾重重,时常内斗。',
},
{
id: 'obsidian',
name: '黑曜石国际 (Obsidian)',
leader: 'Ash (灰烬)',
ideology: '股东利益至上',
type: 'pmc',
image: 'https://files.catbox.moe/bqbp8k.png',
description: '顶尖跨国私人军事承包商。泛亚矿业集团的打手,西方跨国资本的“白手套”。以极高的效率和零失误率闻名。',
},
{
id: 'bloodsworn',
name: '血誓者 (The Bloodsworn)',
leader: '“先知”',
ideology: '极端民族主义与原教旨主义',
type: 'extremist',
image: 'https://files.catbox.moe/d09upc.png',
description: '仇视一切“外来者”和被污染的本地人。手段极其残忍,频繁制造无差别恐怖袭击。',
}
];const REGION_DESC = "锡尔克斯坦:一个位于中亚的内陆山国,因蕴藏着全球储量惊人的稀土矿产“硒银”而成为地缘政治的棋盘。连绵的山脉与荒凉的戈壁是其主要地貌。";
const SQUAD_DESC = "MSF驻巴雷兹特遣队 – 代号“红隼” (Kestrel Squad) MSF总部眼中的“刺头”与“英雄”,也是最想撤销却又不敢撤销的传奇编制。他们驻扎在战火最密集的巴雷兹前线,是方圆几百公里内唯一的外科手术力量。有“流氓小队”的恶名。";// — COMPONENTS —
const NavBar = ({ current, onNavigate }) => {
const items = [
{ id: 'HERO', label: '档案/PROFILE', icon: User },
{ id: 'SCENARIO', label: '入梦/COMMS', icon: MessageSquare },
{ id: 'SQUAD', label: '红隼小队/SQUAD', icon: Users },
{ id: 'FACTIONS', label: '情报/INTEL', icon: MapIcon },
];return (
<nav className="fixed bottom-0 left-0 w-full z-50 md:static md:w-20 md:h-full md:z-auto bg-mil-dark border-t md:border-t-0 md:border-r border-slate-700 flex md:flex-col justify-around md:justify-center items-center py-2 md:py-0 bg-opacity-95 md:bg-opacity-90 md:backdrop-blur-sm">
<div className="hidden md:block absolute top-6 text-mil-accent font-military text-xl tracking-widest rotate-90 origin-left translate-x-12 opacity-50">
MSF-DB
</div>{items.map((item) => {
const isActive = current === item.id;
const IconComponent = item.icon;return (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
className={`
relative p-3 md:p-4 transition-all duration-300 group
flex flex-col items-center gap-1
${isActive ? 'text-mil-accent' : 'text-slate-500 hover:text-slate-300'}
`}
>
<div className={`
absolute inset-0 bg-mil-accent opacity-0 transition-opacity duration-300
${isActive ? 'opacity-10' : 'group-hover:opacity-5'}
rounded-lg
`}></div><div className={`
absolute md:left-0 md:top-1/2 md:-translate-y-1/4 md:w-1 md:h-8
top-0 left-1/2 -translate-x-1/4 h-0.5 w-8
bg-mil-accent transition-all duration-300
${isActive ? 'opacity-100 scale-100' : 'opacity-0 scale-0'}
`}></div><div className="transform transition-transform duration-200 group-hover:scale-110">
<IconComponent size={18} />
</div>
<span className="text-[10px] md:text-[9px] font-tech tracking-wider opacity-80">
{item.label.split('/')[0]}
</span>
</button>
);
})}
</nav>
);
};const HeroSection = () => {
const [isFlipped, setIsFlipped] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);const handleFlip = (e) => {
if (e) e.stopPropagation();
if (isAnimating) return;setIsFlipped(!isFlipped);
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 700);
};// Helper to determine if we are in the static "Back View" state
const showBackStatic = isFlipped && !isAnimating;return (
<div className="w-full h-full flex items-center justify-center p-4 md:p-8 pb-24 md:pb-8">
<div className="perspective-1000 max-h-[100vh] w-full max-w-[85vw] md:max-w-md aspect-[9/16] md:aspect-[3/5] relative">
<div className={`
relative w-full h-full transition-all duration-700
${isAnimating ? 'transform-style-3d' : ''}
${isFlipped ? 'rotate-y-180' : ''}
`}>{/* Front Side */}
<div
className={`
absolute inset-0 w-full h-full bg-mil-panel border-2 border-slate-600 shadow-2xl overflow-hidden group rounded-sm cursor-pointer
${showBackStatic ? 'invisible' : 'backface-hidden'}
`}
onClick={handleFlip}
>
<div className="absolute top-0 left-0 w-full z-30 flex justify-between items-center p-3 bg-gradient-to-b from-black/80 to-transparent">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-mil-green animate-pulse"></div>
<span className="text-[10px] font-mono text-mil-green tracking-widest">{HERO_DATA.basicInfo.status}</span>
</div>
<div className="flex gap-1">
{HERO_DATA.tags.slice(0, 2).map((tag, i) => (
<span key={i} className="text-[9px] px-1 bg-slate-800/80 text-slate-300 border border-slate-600 rounded">
{tag}
</span>
))}
</div>
</div><img
src={HERO_DATA.image}
alt={HERO_DATA.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/><div className="absolute top-[17%] right-2 z-20 flex flex-col items-end gap-1 opacity-70">
<div className="flex items-center gap-1 text-mil-red">
<HeartPulse size={14} />
<span className="text-xs font-mono">BPM: 72</span>
</div>
<div className="flex items-center gap-1 text-mil-accent">
<Fingerprint size={14} />
<span className="text-xs font-mono">ID: GC-031</span>
</div>
</div><div className="absolute bottom-0 left-0 w-full z-20 bg-gradient-to-t from-mil-dark via-mil-dark/90 to-transparent pt-12 pb-6 px-6">
<div className="flex justify-between items-end border-b-2 border-mil-red mb-3 pb-2">
<div>
<h1 className="text-3xl md:text-4xl font-military text-mil-light tracking-wide uppercase leading-none">
{HERO_DATA.name.split(' ')[0]}
</h1>
<div className="flex items-center gap-2 mt-1">
<span className="text-sm md:text-lg font-tech text-mil-red uppercase font-bold tracking-widest">
{HERO_DATA.alias.split('/')[1]}
</span>
<span className="text-[10px] md:text-xs text-slate-500 font-mono"> // {HERO_DATA.alias.split('/')[0]}</span>
</div>
</div>
<div className="text-mil-light/20 group-hover:text-mil-accent transition-colors duration-300">
<RefreshCcw size={24} />
</div>
</div><div className="flex justify-between items-start text-xs font-mono text-slate-400">
<div className="flex flex-col gap-0.5">
<span className="text-slate-500">ROLE</span>
<span className="text-slate-200">{HERO_DATA.role}</span>
</div>
<div className="flex flex-col gap-0.5 text-right">
<span className="text-slate-500">BLOOD TYPE</span>
<span className="text-mil-red font-bold">{HERO_DATA.basicInfo.bloodType}</span>
</div>
</div>
</div>
</div>{/* Back Side */}
<div
className={`
absolute inset-0 w-full h-full rotate-y-180 bg-slate-900 border-2 border-mil-accent shadow-2xl flex flex-col overflow-hidden rounded-sm
${showBackStatic ? '' : 'backface-hidden'}
`}
><div
className="p-4 bg-mil-panel border-b border-slate-700 flex justify-between items-center shrink-0 cursor-pointer hover:bg-slate-800 transition-colors z-10 relative"
onClick={handleFlip}
>
<div>
<h3 className="text-xl font-military text-mil-accent">PERSONNEL FILE</h3>
<p className="text-[10px] font-mono text-slate-500">CLEARANCE: OFFICER ONLY</p>
</div>
<div className="w-8 h-8 rounded border border-slate-600 flex items-center justify-center bg-slate-800">
<FileText size={16} className="text-slate-400" />
</div>
</div><div className="flex-1 overflow-y-auto p-5 touch-pan-y overscroll-contain pointer-events-auto cursor-auto relative z-0">
<div className="mb-6 relative">
<div className="absolute -left-3 top-0 bottom-0 w-1 bg-mil-red"></div>
<p className="font-serif italic text-slate-300 text-sm leading-relaxed">
"{HERO_DATA.quote}"
</p>
</div><div className="grid grid-cols-2 gap-3 mb-6">
{HERO_DATA.attributes.map((stat, idx) => (
<div key={idx} className="bg-slate-800/50 p-2 border border-slate-700/50 rounded">
<div className="flex justify-between items-center mb-1">
<span className="text-[9px] font-mono text-slate-400 tracking-wider">{stat.label}</span>
<span className="text-[9px] font-bold text-white">{stat.value}</span>
</div>
<div className="h-1 bg-slate-700 rounded-full overflow-hidden">
<div className={`h-full ${stat.color}`} style={{ width: `${stat.value}%` }}></div>
</div>
</div>
))}
</div><div className="mb-6">
<h4 className="text-xs font-bold text-mil-accent font-tech tracking-widest mb-2 flex items-center gap-2">
<Activity size={12} /> PSYCH PROFILE
</h4>
<p className="text-xs md:text-sm text-slate-400 font-sans leading-relaxed text-justify">
{HERO_DATA.psychProfile}
</p>
<div className="mt-2 flex flex-wrap gap-1">
{HERO_DATA.tags.map(t => (
<span key={t} className="text-[9px] px-1.5 py-0.5 border border-slate-600 text-slate-500 rounded">{t}</span>
))}
</div>
</div><div className="mb-6">
<h4 className="text-xs font-bold text-mil-text font-tech tracking-widest mb-2 flex items-center gap-2">
<Briefcase size={12} /> SERVICE RECORD
</h4>
<div className="text-xs text-slate-400 font-sans leading-relaxed border-l border-slate-700 pl-3">
{HERO_DATA.background}
</div>
</div><div className="mb-2">
<h4 className="text-xs font-bold text-mil-text font-tech tracking-widest mb-2 flex items-center gap-2">
<Pill size={12} /> TYPICAL LOADOUT
</h4>
<div className="grid grid-cols-1 gap-1">
{HERO_DATA.loadout.map((item, idx) => (
<div key={idx} className="text-[10px] font-mono text-slate-500 flex items-center gap-2">
<span className="w-1 h-1 bg-mil-red rounded-full"></span>
{item}
</div>
))}
</div>
</div>
</div><div
className="p-2 bg-slate-900 border-t border-slate-800 shrink-0 text-center cursor-pointer hover:bg-slate-800/50 transition-colors z-10 relative"
onClick={handleFlip}
>
<span className="text-[9px] font-mono text-mil-accent animate-pulse">TAP BAR TO CLOSE DOSSIER</span>
</div>
</div>
</div>
</div>
</div>
);
};const ScenarioSection = () => {
const [loadingId, setLoadingId] = useState(null);
const [displayScenarios, setDisplayScenarios] = useState(SCENARIOS);
const [isInitializing, setIsInitializing] = useState(true);useEffect(() => {
const loadGreetings = async () => {
try {
if (typeof window.getChatMessages !== 'function') {
// Fallback
setDisplayScenarios(SCENARIOS);
setIsInitializing(false);
return;
}const messages = await window.getChatMessages("0", { include_swipe: true });
let swipes = [];
// Handle array vs object return type from ST API
if (Array.isArray(messages) && messages.length > 0 && messages[0].swipes) {
swipes = messages[0].swipes;
} else if (messages && messages.swipes) {
swipes = messages.swipes;
}if (!swipes || swipes.length <= 1) {
setDisplayScenarios(SCENARIOS);
setIsInitializing(false);
return;
}const loadedScenarios = [];
// Start from 1 to skip default greeting
for (let i = 1; i < swipes.length; i++) {
const content = swipes[i];
const metaIndex = i – 1;
const staticMeta = SCENARIOS[metaIndex];if (staticMeta) {
loadedScenarios.push({
…staticMeta,
content: content,
swipeId: i
});
} else {
loadedScenarios.push({
id: `dynamic_${i}`,
title: `LOG_FRAGMENT_${String(i).padStart(3, '0')}`,
content: content,
swipeId: i,
tags: ['RECOVERED_DATA']
});
}
}
setDisplayScenarios(loadedScenarios);
} catch (error) {
console.error('Failed to load greetings:', error);
setDisplayScenarios(SCENARIOS);
} finally {
setIsInitializing(false);
}
};
loadGreetings();
}, []);const handleScenarioClick = async (id, content, swipeId) => {
if (loadingId) return;
setLoadingId(id);setTimeout(async () => {
try {
if (window.setChatMessage) {
await window.setChatMessage(content, 0, { swipe_id: swipeId });
} else {
alert(`Simulated: Scenario loaded.n${content.substring(0, 30)}…`);
}
} catch (error) {
console.error('切换失败:', error);
alert('Switch failed.');
} finally {
setLoadingId(null);
}
}, 500);
};return (
<div className="w-full h-full p-6 md:p-12 overflow-y-auto pb-24 md:pb-12">
<div className="mb-8 border-l-4 border-mil-accent pl-4">
<h2 className="text-3xl font-military text-white uppercase">Incoming Transmissions</h2>
<p className="text-mil-text font-tech text-sm mt-1">
{isInitializing ? 'SCANNING FREQUENCIES…' : 'Select an entry point for simulation.'}
</p>
</div>{isInitializing ? (
<div className="flex flex-col items-center justify-center h-48 text-mil-accent/50 gap-4">
<Loader2 className="animate-spin" size={32} />
<span className="font-mono text-xs tracking-widest">ESTABLISHING LINK…</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{displayScenarios.map((scenario) => (
<div
key={scenario.id}
onClick={() => handleScenarioClick(scenario.id, scenario.content, scenario.swipeId)}
className="group relative bg-mil-panel border border-slate-700 hover:border-mil-accent transition-all duration-300 cursor-pointer overflow-hidden hover:shadow-[0_0_20px_rgba(217,119,6,0.15)]"
>
<div className="absolute inset-0 opacity-10 bg-grid-pattern pointer-events-none"></div><div className="p-6 relative z-10">
<div className="flex justify-between items-start mb-4">
<div className="flex gap-2">
{scenario.tags.map(tag => (
<span key={tag} className="text-[10px] px-1.5 py-0.5 bg-slate-800 text-slate-300 uppercase tracking-wide border border-slate-600">
{tag}
</span>
))}
</div>
<Radio className={`text-mil-accent ${loadingId === scenario.id ? 'animate-pulse' : 'opacity-50'}`} size={16} />
</div><h3 className="text-xl font-bold font-sans text-mil-light mb-2 group-hover:text-mil-accent transition-colors">
{scenario.title}
</h3><div className="h-px w-full bg-gradient-to-r from-slate-700 via-slate-600 to-transparent my-4"></div>
<div className="flex items-center justify-end gap-2 text-xs font-mono text-slate-500 group-hover:text-mil-light transition-colors">
{loadingId === scenario.id ? (
<>
<Loader2 className="animate-spin" size={14} />
<span>UPLOADING…</span>
</>
) : (
<>
<Play size={14} className="fill-current" />
<span>INITIATE</span>
</>
)}
</div>
</div><div className="absolute inset-0 bg-mil-accent/5 translate-y-full group-hover:translate-y-0 transition-transform duration-300 pointer-events-none"></div>
</div>
))}
</div>
)}
</div>
);
};const DatabaseViewer = ({ title, subtitle, data, type }) => {
const [selectedId, setSelectedId] = useState(null);
const [imageLoading, setImageLoading] = useState(true);const selectedItem = data.find(item => item.id === selectedId) || null;
useEffect(() => {
if (selectedId) setImageLoading(true);
}, [selectedId]);return (
<div className="w-full h-full flex flex-col md:flex-row overflow-hidden">
{/* List */}
<div className={`
flex-col bg-mil-panel/50 border-r border-slate-700 h-full
${selectedId ? 'hidden md:flex md:w-1/3' : 'flex w-full md:w-1/3'}
`}>
<div className="p-6 border-b border-slate-700 bg-mil-dark/50 shrink-0">
<h2 className="text-2xl font-military text-mil-accent uppercase">{title}</h2>
<p className="text-xs font-mono text-slate-500 mt-1">{subtitle}</p>
</div><div className="flex-1 overflow-y-auto p-2 space-y-2 pb-24 md:pb-2">
{data.map((item) => (
<button
key={item.id}
onClick={() => setSelectedId(item.id)}
className={`
w-full text-left p-4 border border-transparent transition-all duration-200 group
flex items-center justify-between
${selectedId === item.id
? 'bg-mil-accent/10 border-mil-accent text-mil-light'
: 'hover:bg-slate-800 text-slate-400 border-slate-800'}
`}
>
<div>
<div className={`font-bold font-sans ${selectedId === item.id ? 'text-mil-accent' : ''}`}>
{item.name}
</div>
<div className="text-xs font-mono opacity-60 truncate max-w-[200px]">
{item.role || item.leader}
</div>
</div>
<ChevronRight
size={16}
className={`transition-transform duration-300 ${selectedId === item.id ? 'opacity-100 translate-x-1' : 'opacity-0'}`}
/>
</button>
))}
</div>
</div>{/* Details */}
<div className={`
bg-slate-900/80 relative flex flex-col h-full
${selectedId ? 'flex w-full md:w-2/3' : 'hidden md:flex md:w-2/3'}
`}>
<div className="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>{selectedId && (
<button
onClick={() => setSelectedId(null)}
className="md:hidden flex items-center gap-2 p-4 bg-mil-panel border-b border-slate-700 text-mil-accent font-tech tracking-wider sticky top-0 z-20"
>
<ArrowLeft size={16} />
<span>RETURN TO LIST</span>
</button>
)}<div className="flex-1 overflow-y-auto p-6 md:p-12 pb-24 md:pb-12">
{selectedItem ? (
<div className="animate-fadeIn max-w-2xl mx-auto">
<div className="flex items-center gap-4 mb-6">
<div className="shrink-0 relative">
{selectedItem.image ? (
<>
<img
key={selectedItem.id}
src={selectedItem.image}
alt={selectedItem.name}
onLoad={() => setImageLoading(false)}
className={`
w-16 h-16 md:w-24 md:h-24 rounded border border-slate-600 bg-slate-800 transition-all duration-300
${type === 'SQUAD'
? 'object-cover filter contrast-125 saturate-50 hover:saturate-100'
: 'object-contain p-1'
}
${imageLoading ? 'hidden' : 'block'}
`}
/>
{imageLoading && (
<div className="w-16 h-16 md:w-24 md:h-24 p-3 bg-slate-800 rounded border border-slate-600 shrink-0 flex items-center justify-center animate-pulse">
{type === 'SQUAD' ? <User className="text-slate-600" size={32} /> : <Shield className="text-slate-600" size={32} />}
</div>
)}
</>
) : (
<div className="p-3 bg-slate-800 rounded border border-slate-600 shrink-0">
{type === 'SQUAD' ? <User className="text-mil-accent" size={32} /> : <Shield className="text-mil-red" size={32} />}
</div>
)}
</div>
<div>
<h3 className="text-2xl md:text-3xl font-bold text-white font-sans">{selectedItem.name}</h3>
<div className="flex flex-col md:flex-row md:gap-4 mt-2 text-sm font-mono text-mil-accent">
{type === 'SQUAD' ? (
<>
<span>ROLE: {selectedItem.role}</span>
<span className="hidden md:inline text-slate-500">|</span>
<span>NAT: {selectedItem.nationality}</span>
</>
) : (
<>
<span>LEADER: {selectedItem.leader}</span>
<span className="hidden md:inline text-slate-500">|</span>
<span>TYPE: {selectedItem.type.toUpperCase()}</span>
</>
)}
</div>
</div>
</div><div className="space-y-6">
{type === 'FACTION' && (
<div className="bg-mil-panel p-4 border-l-2 border-mil-red">
<span className="text-xs font-bold text-slate-500 block mb-1">IDEOLOGY</span>
<span className="text-lg text-slate-300 font-serif italic">"{selectedItem.ideology}"</span>
</div>
)}{selectedItem.quote && (
<div className="bg-mil-panel/50 p-4 border-l-2 border-mil-accent">
<span className="text-xs font-bold text-mil-accent block mb-2 font-mono flex items-center gap-2">
<MessageSquare size={12} /> VOICE LOG // TRANSCRIPT
</span>
<p className="text-sm md:text-base text-slate-300 font-serif italic leading-relaxed whitespace-pre-wrap">
{selectedItem.quote}
</p>
</div>
)}<div className="relative mt-8">
<span className="absolute -top-3 left-0 bg-slate-900 px-2 text-xs font-mono text-mil-accent">
DESCRIPTION
</span>
<div className="border border-slate-700 p-6 text-slate-300 leading-relaxed font-sans text-sm md:text-base bg-slate-900/50">
{selectedItem.description}
</div>
</div>
</div></div>
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-600 opacity-50">
<div className="w-16 h-16 border-2 border-dashed border-slate-600 rounded-full animate-spin-slow mb-4"></div>
<p className="font-mono text-sm tracking-widest">AWAITING SELECTION…</p>
</div>
)}
</div>
</div>
</div>
);
};// — APP —
const App = () => {
const [currentSection, setCurrentSection] = useState('HERO');const renderContent = () => {
switch (currentSection) {
case 'HERO':
return <HeroSection />;
case 'SCENARIO':
return <ScenarioSection />;
case 'SQUAD':
return (
<div className="w-full h-full flex flex-col">
<div className="bg-mil-panel border-b border-slate-700 p-4 md:p-6 shrink-0">
<h3 className="text-mil-accent font-military text-xl mb-2">UNIT: KESTREL SQUAD</h3>
<p className="text-xs md:text-sm text-slate-400 font-mono leading-relaxed max-w-4xl">
{SQUAD_DESC}
</p>
</div>
<div className="flex-1 min-h-0">
<DatabaseViewer
key="SQUAD"
title="Kestrel Squad"
subtitle="MSF 驻巴雷兹特遣队 // PERSONNEL_DB"
data={SQUAD_MEMBERS}
type="SQUAD"
/>
</div>
</div>
);
case 'FACTIONS':
return (
<div className="w-full h-full flex flex-col">
<div className="bg-mil-panel border-b border-slate-700 p-4 md:p-6 shrink-0">
<h3 className="text-mil-accent font-military text-xl mb-2">REGION: SIRKESTAN</h3>
<p className="text-xs md:text-sm text-slate-400 font-mono leading-relaxed max-w-4xl">
{REGION_DESC}
</p>
</div>
<div className="flex-1 min-h-0">
<DatabaseViewer
key="FACTIONS"
title="Conflict Forces"
subtitle="FACTION INTELLIGENCE // CLASSIFIED"
data={FACTIONS}
type="FACTION"
/>
</div>
</div>
);
default:
return <HeroSection />;
}
};return (
<div className="fixed inset-0 w-full h-full bg-mil-dark text-slate-200 overflow-hidden flex flex-col md:flex-row">
<NavBar current={currentSection} onNavigate={setCurrentSection} /><main className="flex-1 h-full relative overflow-hidden bg-slate-900">
<div className="absolute top-4 right-4 w-24 h-24 border-t border-r border-slate-700 pointer-events-none opacity-50 z-0"></div>
<div className="absolute bottom-20 right-4 w-12 h-12 border-b border-r border-slate-700 pointer-events-none opacity-50 z-0 md:bottom-4"></div><div className="w-full h-full relative z-10 animate-fadeIn">
{renderContent()}
</div>
</main>
</div>
);
};const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
</script>
</body>
</html>
“`