Files
aibis-dream/prototype/huoshan_prototype.html
T
2026-01-13 19:20:06 +08:00

2110 lines
82 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>火山修复流程 - 原型验证</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e8e8e8;
overflow-x: hidden;
}
/* 主容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
background: rgba(255,255,255,0.05);
border-radius: 12px;
margin-bottom: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.status-bar h1 {
font-size: 1.4em;
font-weight: 500;
color: #64ffda;
}
.phase-indicator {
display: flex;
gap: 8px;
}
.phase-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
transition: all 0.3s;
}
.phase-dot.active {
background: #64ffda;
box-shadow: 0 0 10px #64ffda;
}
.phase-dot.completed {
background: #4ade80;
}
/* 主要内容区 */
.main-content {
display: grid;
grid-template-columns: 280px 1fr 280px;
gap: 20px;
min-height: 600px;
}
/* 左侧面板 - 模块选择 */
.module-panel {
background: rgba(255,255,255,0.03);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255,255,255,0.08);
}
.module-panel h2 {
font-size: 1em;
color: #94a3b8;
margin-bottom: 15px;
font-weight: 400;
}
.module-btn {
width: 100%;
padding: 15px;
margin-bottom: 12px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px;
color: #e8e8e8;
cursor: pointer;
transition: all 0.3s;
text-align: left;
font-size: 0.95em;
}
.module-btn:hover {
background: rgba(255,255,255,0.1);
border-color: rgba(100,255,218,0.3);
}
.module-btn.active {
background: rgba(100,255,218,0.15);
border-color: #64ffda;
}
.module-btn.locked {
opacity: 0.5;
cursor: not-allowed;
}
.module-btn .status {
font-size: 0.8em;
color: #64ffda;
margin-top: 5px;
}
.module-btn .status.warning {
color: #fbbf24;
}
.module-btn .status.locked {
color: #ef4444;
}
/* 中央区域 - 游戏/内容区 */
.game-area {
background: rgba(0,0,0,0.3);
border-radius: 16px;
position: relative;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
}
.game-canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
/* 诊断界面 */
.diagnosis-view {
padding: 30px;
height: 100%;
display: flex;
flex-direction: column;
}
.diagnosis-title {
font-size: 1.3em;
color: #64ffda;
margin-bottom: 20px;
}
.diagnosis-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
}
.data-card {
background: rgba(255,255,255,0.05);
border-radius: 10px;
padding: 15px;
border: 1px solid rgba(255,255,255,0.1);
}
.data-card h3 {
font-size: 0.9em;
color: #94a3b8;
margin-bottom: 10px;
}
.data-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.data-row:last-child {
border-bottom: none;
}
.data-label {
color: #94a3b8;
}
.data-value {
color: #e8e8e8;
}
.data-value.normal {
color: #4ade80;
}
.data-value.warning {
color: #fbbf24;
}
.data-value.error {
color: #ef4444;
}
/* 波形图 */
.waveform-container {
height: 100px;
background: rgba(0,0,0,0.3);
border-radius: 8px;
position: relative;
overflow: hidden;
margin: 10px 0;
}
.waveform {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 60px;
transform: translateY(-50%);
}
/* 情绪对比选择器 */
.emotion-selector {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 15px;
}
.emotion-option {
padding: 12px;
background: rgba(255,255,255,0.05);
border: 2px solid rgba(255,255,255,0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.emotion-option:hover {
background: rgba(255,255,255,0.1);
}
.emotion-option.selected {
border-color: #64ffda;
background: rgba(100,255,218,0.1);
}
.emotion-option .match {
font-size: 0.8em;
color: #64ffda;
margin-top: 5px;
display: none;
}
.emotion-option.selected .match {
display: block;
}
/* 操作按钮 */
.action-btn {
padding: 12px 24px;
background: linear-gradient(135deg, #64ffda 0%, #4ade80 100%);
border: none;
border-radius: 8px;
color: #1a1a2e;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(100,255,218,0.3);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* 右侧面板 - 信号分析 */
.pathway-panel {
background: rgba(255,255,255,0.03);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255,255,255,0.08);
}
.pathway-panel h2 {
font-size: 1em;
color: #94a3b8;
margin-bottom: 15px;
font-weight: 400;
}
/* 拔河条设计 */
.tugofwar-container {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 20px 15px;
margin-bottom: 15px;
}
.tugofwar-labels {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85em;
}
.tugofwar-label-left {
color: #4ade80;
}
.tugofwar-label-right {
color: #f87171;
}
.tugofwar-bar {
height: 12px;
background: linear-gradient(90deg,
rgba(74, 222, 128, 0.3) 0%,
rgba(255,255,255,0.1) 50%,
rgba(248, 113, 113, 0.3) 100%);
border-radius: 6px;
position: relative;
overflow: visible;
}
.tugofwar-indicator {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
background: #fff;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: left 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.tugofwar-indicator.winning-left {
background: linear-gradient(135deg, #4ade80, #22c55e);
box-shadow: 0 0 12px rgba(74, 222, 128, 0.5);
}
.tugofwar-indicator.winning-right {
background: linear-gradient(135deg, #f87171, #ef4444);
box-shadow: 0 0 12px rgba(248, 113, 113, 0.5);
}
.tugofwar-indicator.balanced {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
}
.tugofwar-marks {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 0.7em;
color: #64748b;
}
.tugofwar-status {
text-align: center;
margin-top: 15px;
padding: 10px;
background: rgba(255,255,255,0.03);
border-radius: 8px;
font-size: 0.9em;
}
/* 未分析状态 */
.analysis-pending {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.analysis-pending .icon {
font-size: 2em;
margin-bottom: 15px;
opacity: 0.5;
}
/* 分析动画 */
.analyzing {
text-align: center;
padding: 30px 20px;
}
.analyzing .spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: #64ffda;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 语言生成显示器 */
.language-display {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
border: 1px solid rgba(100, 255, 218, 0.3);
border-radius: 8px;
padding: 12px 24px;
z-index: 15;
text-align: center;
min-width: 200px;
}
.display-label {
font-size: 0.75em;
color: #64748b;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 1px;
}
.display-text {
font-size: 1.4em;
color: #64ffda;
font-family: 'Courier New', monospace;
min-height: 1.5em;
}
.display-text.error {
color: #f87171;
}
.display-text.generating {
animation: textBlink 0.5s ease-in-out infinite;
}
@keyframes textBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* 对话框 */
.dialog-box {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0,0,0,0.95), rgba(0,0,0,0.8));
padding: 20px 30px;
border-top: 1px solid rgba(255,255,255,0.1);
transform: translateY(100%);
transition: transform 0.3s ease;
}
.dialog-box.show {
transform: translateY(0);
}
.dialog-speaker {
color: #64ffda;
font-size: 0.9em;
margin-bottom: 8px;
}
.dialog-text {
font-size: 1.1em;
line-height: 1.6;
max-width: 800px;
}
.dialog-continue {
position: absolute;
right: 30px;
bottom: 20px;
color: #64ffda;
font-size: 0.85em;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
/* 粒子游戏区域 */
.particle-game {
width: 100%;
height: 100%;
position: relative;
}
.particle {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
cursor: default;
transition: transform 0.1s;
user-select: none;
}
.particle.target {
background: radial-gradient(circle at 30% 30%, #ff6b6b, #c0392b);
box-shadow: 0 0 20px rgba(255,107,107,0.5);
}
.particle.non-target {
background: radial-gradient(circle at 30% 30%, #74b9ff, #0984e3);
box-shadow: 0 0 15px rgba(116,185,255,0.4);
}
.particle.constructing {
background: radial-gradient(circle at 30% 30%, #64ffda, #00b894);
box-shadow: 0 0 25px rgba(100,255,218,0.6);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* 连接线 SVG */
.connections-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.connection-line {
stroke: rgba(255,107,107,0.6);
stroke-width: 2;
fill: none;
}
/* 游戏UI覆盖层 */
.game-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 15px 20px;
background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent);
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
.game-instruction {
font-size: 0.9em;
color: #94a3b8;
}
.game-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-label {
font-size: 0.85em;
color: #94a3b8;
}
.progress-bar {
width: 150px;
height: 6px;
background: rgba(255,255,255,0.1);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #64ffda;
transition: width 0.3s;
}
/* 保护模式特殊UI */
.protection-mode .game-instruction {
color: #fbbf24;
}
.construction-progress {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.construction-text {
font-size: 1.5em;
color: #64ffda;
margin-bottom: 10px;
text-shadow: 0 0 20px rgba(100,255,218,0.5);
}
.construction-bar {
width: 300px;
height: 10px;
background: rgba(255,255,255,0.1);
border-radius: 5px;
overflow: hidden;
}
.construction-fill {
height: 100%;
background: linear-gradient(90deg, #64ffda, #4ade80);
transition: width 0.3s;
}
/* 隐藏元素 */
.hidden {
display: none !important;
}
/* 结果屏幕 */
.result-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s;
}
.result-screen.show {
opacity: 1;
pointer-events: auto;
}
.result-text {
font-size: 1.5em;
color: #64ffda;
margin-bottom: 20px;
}
.result-detail {
color: #94a3b8;
margin-bottom: 30px;
}
/* 开场界面 */
.intro-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.5s;
}
.intro-screen.hidden {
opacity: 0;
pointer-events: none;
}
.intro-title {
font-size: 2.5em;
color: #64ffda;
margin-bottom: 20px;
}
.intro-subtitle {
font-size: 1.2em;
color: #94a3b8;
margin-bottom: 40px;
}
.intro-btn {
padding: 15px 40px;
font-size: 1.1em;
}
/* 火山说话气泡 */
.huoshan-bubble {
position: absolute;
top: 20px;
right: 20px;
max-width: 250px;
padding: 15px;
background: rgba(255,107,107,0.15);
border: 1px solid rgba(255,107,107,0.3);
border-radius: 12px;
color: #ff6b6b;
font-size: 0.9em;
opacity: 0;
transition: opacity 0.3s;
}
.huoshan-bubble.show {
opacity: 1;
}
</style>
</head>
<body>
<!-- 开场界面 -->
<div class="intro-screen" id="introScreen">
<div class="intro-title">🌋 火山修复流程</div>
<div class="intro-subtitle">语言表达模块诊断与修复原型</div>
<button class="action-btn intro-btn" onclick="startGame()">开始诊断</button>
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.2);">
<div style="color: #94a3b8; margin-bottom: 15px;">测试面板(跳过流程直接测试)</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;">
<button class="action-btn" style="background: #475569; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('voice')">发声单元</button>
<button class="action-btn" style="background: #475569; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('trigger')">触发器</button>
<button class="action-btn" style="background: #6366f1; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('processor')">处理器观察</button>
<button class="action-btn" style="background: #475569; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('game1')">第一次清理</button>
<button class="action-btn" style="background: #475569; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('game2')">第二次清理</button>
<button class="action-btn" style="background: #475569; padding: 8px 16px; font-size: 0.9em;" onclick="testModule('game3')">第三次清理</button>
</div>
</div>
</div>
<div class="container">
<!-- 顶部状态栏 -->
<div class="status-bar">
<h1>🌋 火山 - 表达模块诊断</h1>
<div class="phase-indicator">
<div class="phase-dot" id="phase1"></div>
<div class="phase-dot" id="phase2"></div>
<div class="phase-dot" id="phase3"></div>
<div class="phase-dot" id="phase4"></div>
<div class="phase-dot" id="phase5"></div>
</div>
</div>
<!-- 主要内容区 -->
<div class="main-content">
<!-- 左侧面板 - 模块选择 -->
<div class="module-panel">
<h2>表达模块结构</h2>
<button class="module-btn" id="btnTrigger" onclick="showModule('trigger')">
📊 触发器(情绪输入)
<div class="status" id="triggerStatus">待检查</div>
</button>
<button class="module-btn" id="btnVoice" onclick="showModule('voice')">
🔊 发声单元(输出)
<div class="status" id="voiceStatus">待检查</div>
</button>
<button class="module-btn locked" id="btnProcessor" onclick="showModule('processor')">
⚙️ 处理器(语言生成)
<div class="status locked" id="processorStatus">🔒 需要收集数据</div>
</button>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
<h2>游戏阶段</h2>
<button class="module-btn" id="btnGame1" onclick="startParticleGame(1)" style="display:none;">
🎮 第一次清理
</button>
<button class="module-btn" id="btnGame2" onclick="startParticleGame(2)" style="display:none;">
🎮 第二次清理
</button>
<button class="module-btn" id="btnGame3" onclick="startParticleGame(3)" style="display:none;">
🎮 第三次清理
</button>
</div>
</div>
<!-- 中央区域 -->
<div class="game-area" id="gameArea">
<!-- 欢迎界面 -->
<div class="diagnosis-view" id="welcomeView">
<div class="diagnosis-title">表达模块诊断系统</div>
<div class="diagnosis-content">
<div class="data-card">
<h3>患者信息</h3>
<div class="data-row">
<span class="data-label">编号</span>
<span class="data-value">火山 (HUOSHAN-001)</span>
</div>
<div class="data-row">
<span class="data-label">类型</span>
<span class="data-value">销售机器人</span>
</div>
<div class="data-row">
<span class="data-label">症状</span>
<span class="data-value warning">不停说笑话、结巴</span>
</div>
</div>
<div class="data-card">
<h3>诊断流程</h3>
<p style="color: #94a3b8; line-height: 1.8;">
1. 检查<strong>发声单元</strong>,提取语音参数<br>
2. 检查<strong>触发器</strong>,分析情绪波形<br>
3. 进入<strong>处理器</strong>,定位并修复问题
</p>
</div>
<p style="color: #64ffda; margin-top: auto;">
请从左侧选择模块开始诊断
</p>
</div>
</div>
<!-- 发声单元界面 -->
<div class="diagnosis-view hidden" id="voiceView">
<div class="diagnosis-title">🔊 发声单元 - 语音参数提取</div>
<div class="diagnosis-content">
<div class="data-card">
<h3>提取语音数据</h3>
<p style="color: #94a3b8; margin-bottom: 15px;">
需要提取语音参数以供处理器定位问题
</p>
<button class="action-btn" id="extractBtn" onclick="extractVoiceData()">
开始提取
</button>
</div>
<div class="data-card hidden" id="voiceResult">
<h3>提取结果</h3>
<div class="data-row">
<span class="data-label">内容解析</span>
<span class="data-value normal">✓ 正常</span>
</div>
<div class="data-row">
<span class="data-label">语法结构</span>
<span class="data-value normal">✓ 正常</span>
</div>
<div class="data-row">
<span class="data-label">语调参数</span>
<span class="data-value warning">⚠️ 检测到异常</span>
</div>
</div>
<div class="data-card hidden" id="voiceDetail">
<h3>语调异常详情</h3>
<div class="data-row">
<span class="data-label">微颤</span>
<span class="data-value warning">检测到</span>
</div>
<div class="data-row">
<span class="data-label">气息不稳</span>
<span class="data-value warning">检测到</span>
</div>
<div class="data-row">
<span class="data-label">重音偏移</span>
<span class="data-value warning">检测到</span>
</div>
<p style="color: #fbbf24; margin-top: 10px; font-size: 0.9em;">
备注:这些特征与输出内容的情绪不匹配
</p>
</div>
</div>
</div>
<!-- 触发器界面 -->
<div class="diagnosis-view hidden" id="triggerView">
<div class="diagnosis-title">📊 触发器 - 情绪波形分析</div>
<div class="diagnosis-content">
<div class="data-card">
<h3>采集情绪波形</h3>
<button class="action-btn" id="captureBtn" onclick="captureWaveform()">
开始采集
</button>
</div>
<div class="data-card hidden" id="waveformCard">
<h3>当前波形</h3>
<div class="waveform-container">
<canvas id="waveformCanvas" class="waveform"></canvas>
</div>
<p style="color: #94a3b8; font-size: 0.9em;">
请与情绪参考库对比,选择最匹配的情绪类型
</p>
</div>
<div class="hidden" id="emotionSelector">
<h3 style="color: #94a3b8; margin-bottom: 10px;">情绪参考库</h3>
<div class="emotion-selector">
<div class="emotion-option" onclick="selectEmotion(this, 'happy', 12)">
😊 快乐
<div class="match">12% 匹配</div>
</div>
<div class="emotion-option" onclick="selectEmotion(this, 'sad', 35)">
😢 悲伤
<div class="match">35% 匹配</div>
</div>
<div class="emotion-option" onclick="selectEmotion(this, 'fear', 87)">
😰 恐惧
<div class="match">87% 匹配</div>
</div>
<div class="emotion-option" onclick="selectEmotion(this, 'anger', 28)">
😠 愤怒
<div class="match">28% 匹配</div>
</div>
</div>
<button class="action-btn" id="confirmEmotionBtn" onclick="confirmEmotion()" disabled>
确认分析结果
</button>
</div>
<div class="data-card hidden" id="emotionResult">
<h3>分析结论</h3>
<div class="data-row">
<span class="data-label">主导情绪</span>
<span class="data-value error">恐惧 (87%)</span>
</div>
<div class="data-row">
<span class="data-label">次要情绪</span>
<span class="data-value warning">焦虑、不安</span>
</div>
<div class="data-row">
<span class="data-label">情绪强度</span>
<span class="data-value error">高强度负面</span>
</div>
</div>
</div>
</div>
<!-- 处理器解锁界面 -->
<div class="diagnosis-view hidden" id="processorUnlockView">
<div class="diagnosis-title">⚙️ 处理器 - 准备进入</div>
<div class="diagnosis-content">
<div class="data-card">
<h3>数据汇总</h3>
<div class="data-row">
<span class="data-label">触发器(输入)</span>
<span class="data-value error">恐惧/焦虑</span>
</div>
<div class="data-row">
<span class="data-label">发声单元(输出)</span>
<span class="data-value warning">笑话(语调异常)</span>
</div>
<div class="data-row">
<span class="data-label">问题定位</span>
<span class="data-value">输入输出不匹配</span>
</div>
</div>
<div class="data-card" style="background: rgba(100,255,218,0.1); border-color: rgba(100,255,218,0.3);">
<p style="color: #64ffda; line-height: 1.8;">
<strong>诊断结论:</strong><br>
输入的情绪是恐惧...<br>
但输出的是笑话,语调还泄露了真实情绪...<br>
问题一定出在中间的处理过程。
</p>
</div>
<button class="action-btn" onclick="enterProcessor()">
进入处理器
</button>
</div>
</div>
<!-- 粒子游戏界面 -->
<div class="particle-game hidden" id="particleGame">
<svg class="connections-svg" id="connectionsSvg"></svg>
<div id="particlesContainer"></div>
<!-- 语言生成显示器 -->
<div class="language-display hidden" id="languageDisplay">
<div class="display-label">语言生成中</div>
<div class="display-text" id="displayText"></div>
</div>
<div class="game-overlay">
<div class="game-instruction" id="gameInstruction">
左键排斥粒子 | 右键吸引粒子 | 目标:让绿色粒子相连
</div>
<div class="game-progress">
<span class="progress-label">整合度</span>
<div class="progress-bar">
<div class="progress-fill" id="integrationFill" style="width: 0%"></div>
</div>
<span id="integrationPercent">0%</span>
</div>
</div>
<!-- 火山气泡 -->
<div class="huoshan-bubble" id="huoshanBubble"></div>
<!-- 结果屏幕 -->
<div class="result-screen" id="resultScreen">
<div class="result-text" id="resultText">连接成功!</div>
<div class="result-detail" id="resultDetail">火山说出:"有点担心..."</div>
<button class="action-btn" id="resultBtn" onclick="handleResultAction()">继续</button>
</div>
</div>
<!-- 最终结果界面 -->
<div class="diagnosis-view hidden" id="finalView">
<div class="diagnosis-title" style="color: #4ade80;">✨ 修复完成</div>
<div class="diagnosis-content" style="align-items: center; justify-content: center; text-align: center;">
<div style="font-size: 1.3em; color: #64ffda; margin-bottom: 20px;">
✓ 真实表达通路已稳定
</div>
<div style="color: #e8e8e8; line-height: 2; font-size: 1.1em;">
火山:"我害怕被抛弃。"<br>
火山:"我害怕如果不有趣,就没人要我了。"<br>
<br>
<span style="color: #94a3b8;">(停顿)</span><br>
<br>
火山:"说出来之后...好像没那么可怕。"<br>
火山:"谢谢你愿意听我说真话。"
</div>
<button class="action-btn" style="margin-top: 30px;" onclick="location.reload()">
重新开始
</button>
</div>
</div>
</div>
<!-- 右侧面板 - 信号分析 -->
<div class="pathway-panel">
<h2>🔬 信号竞争分析</h2>
<!-- 未分析状态 -->
<div class="analysis-pending" id="analysisPending">
<div class="icon">📊</div>
<div>尚未进行信号分析</div>
<div style="font-size: 0.85em; margin-top: 8px; color: #475569;">
需要在处理器中观察到异常后触发
</div>
</div>
<!-- 分析中动画 -->
<div class="analyzing hidden" id="analyzing">
<div class="spinner"></div>
<div style="color: #64ffda;">追踪信号来源...</div>
<div style="font-size: 0.85em; margin-top: 8px; color: #94a3b8;">
检测竞争路径中
</div>
</div>
<!-- 拔河条(分析完成后显示) -->
<div class="tugofwar-container hidden" id="tugofwarContainer">
<div class="tugofwar-labels">
<span class="tugofwar-label-left">💚 真实表达</span>
<span class="tugofwar-label-right">😂 幽默防御</span>
</div>
<div class="tugofwar-bar">
<div class="tugofwar-indicator winning-right" id="tugofwarIndicator" style="left: 85%;">
</div>
</div>
<div class="tugofwar-marks">
<span></span>
<span>均衡</span>
<span></span>
</div>
<div class="tugofwar-status" id="tugofwarStatus">
<span style="color: #f87171;">幽默防御占据主导</span>
</div>
</div>
<!-- 分析日志 -->
<div class="hidden" id="analysisLog" style="margin-top: 15px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px; font-size: 0.8em; font-family: monospace;">
<div style="color: #64748b; margin-bottom: 8px;">// 分析日志</div>
<div id="logContent" style="color: #94a3b8; line-height: 1.6;"></div>
</div>
</div>
</div>
</div>
<!-- 对话框 -->
<div class="dialog-box" id="dialogBox">
<div class="dialog-speaker" id="dialogSpeaker">医生</div>
<div class="dialog-text" id="dialogText">让我检查一下你的表达模块。</div>
<div class="dialog-continue">点击继续 ▼</div>
</div>
<script>
// 游戏状态
const state = {
currentPhase: 0,
voiceChecked: false,
triggerChecked: false,
processorUnlocked: false,
gameRound: 0,
blockageLevel: 100,
tugofwarPosition: 85, // 0=真实表达, 100=幽默防御
analysisComplete: false,
selectedEmotion: null,
particles: [],
isGameRunning: false,
mousePos: { x: 0, y: 0 },
isMouseDown: false,
mouseButton: 0
};
// 游戏配置
const gameConfigs = [
null, // 占位
{ targetCount: 4, nonTargetCount: 8, targetSentence: "有点担心", reboundTo: 95 },
{ targetCount: 5, nonTargetCount: 10, targetSentence: "我很害怕", reboundTo: 75 },
{ targetCount: 6, nonTargetCount: 8, targetSentence: "我害怕被抛弃", reboundTo: 0 }
];
// 对话队列
let dialogQueue = [];
let dialogCallback = null;
// 开始游戏
function startGame() {
document.getElementById('introScreen').classList.add('hidden');
updatePhase(1);
showDialog([
{ speaker: "系统", text: "患者火山已进入诊疗室。" },
{ speaker: "火山", text: "嘿!你知道销售机器人最怕什么吗?打折季!因为我们的价值也在——哈哈,开玩笑的!" },
{ speaker: "医生", text: "火山,你还好吗?你的表达好像有点...不太对劲。" },
{ speaker: "火山", text: "不对劲?哈哈,你知道'不对劲'和我的销售业绩有什么共同点吗?都——" },
{ speaker: "医生", text: "让我检查一下你的表达模块吧。" }
]);
}
// 测试面板 - 直接跳转到指定模块
function testModule(module) {
document.getElementById('introScreen').classList.add('hidden');
// 预设状态
state.voiceChecked = true;
state.triggerChecked = true;
state.processorUnlocked = true;
document.getElementById('voiceStatus').textContent = '✓ 已提取';
document.getElementById('triggerStatus').textContent = '✓ 已分析';
document.getElementById('btnProcessor').classList.remove('locked');
document.getElementById('processorStatus').textContent = '可以进入';
document.getElementById('processorStatus').classList.remove('locked');
if (module === 'voice') {
showModule('voice');
updatePhase(1);
} else if (module === 'trigger') {
showModule('trigger');
updatePhase(1);
} else if (module === 'processor') {
// 直接进入处理器观察阶段(使用粒子游戏界面)
enterProcessor();
updatePhase(2);
} else if (module === 'game1') {
showAnalysisResult(85);
document.getElementById('btnGame1').style.display = 'block';
startParticleGame(1);
} else if (module === 'game2') {
// 显示拔河条(第二次清理时已经分析过了)
showAnalysisResult(85);
document.getElementById('btnGame2').style.display = 'block';
startParticleGame(2);
} else if (module === 'game3') {
// 显示拔河条
showAnalysisResult(70);
document.getElementById('btnGame3').style.display = 'block';
startParticleGame(3);
}
}
// 直接显示分析结果(用于测试跳转)
function showAnalysisResult(position) {
state.analysisComplete = true;
document.getElementById('analysisPending').classList.add('hidden');
document.getElementById('analyzing').classList.add('hidden');
document.getElementById('tugofwarContainer').classList.remove('hidden');
document.getElementById('analysisLog').classList.remove('hidden');
updateTugofwar(position, false);
}
// 运行信号分析(第一次清理后触发)
function runSignalAnalysis(callback) {
document.getElementById('analysisPending').classList.add('hidden');
document.getElementById('analyzing').classList.remove('hidden');
setTimeout(() => {
document.getElementById('analyzing').classList.add('hidden');
document.getElementById('tugofwarContainer').classList.remove('hidden');
document.getElementById('analysisLog').classList.remove('hidden');
// 添加日志
addAnalysisLog('检测到两条竞争通路');
setTimeout(() => addAnalysisLog('通路A: 真实情绪表达'), 300);
setTimeout(() => addAnalysisLog('通路B: 幽默防御机制'), 600);
setTimeout(() => addAnalysisLog('当前状态: 通路B主导'), 900);
state.analysisComplete = true;
if (callback) setTimeout(callback, 1200);
}, 1500);
}
// 添加分析日志
function addAnalysisLog(text) {
const log = document.getElementById('logContent');
log.innerHTML += `> ${text}<br>`;
log.scrollTop = log.scrollHeight;
}
// 更新拔河条位置(0=真实表达胜, 100=幽默防御胜)
function updateTugofwar(position, animate = true) {
state.tugofwarPosition = position;
const indicator = document.getElementById('tugofwarIndicator');
const status = document.getElementById('tugofwarStatus');
indicator.style.left = position + '%';
// 更新指示器样式
indicator.classList.remove('winning-left', 'winning-right', 'balanced');
if (position < 40) {
indicator.classList.add('winning-left');
status.innerHTML = '<span style="color: #4ade80;">真实表达正在突破!</span>';
} else if (position > 60) {
indicator.classList.add('winning-right');
status.innerHTML = '<span style="color: #f87171;">幽默防御占据主导</span>';
} else {
indicator.classList.add('balanced');
status.innerHTML = '<span style="color: #fbbf24;">双方势均力敌</span>';
}
}
// 拔河回弹效果
function tugofwarRebound(target, rebound, callback) {
updateTugofwar(target);
addAnalysisLog(`信号位移: ${100 - target}%`);
if (rebound > target) {
setTimeout(() => {
updateTugofwar(rebound);
addAnalysisLog('检测到回弹力...');
addAnalysisLog(`回弹至: ${100 - rebound}%`);
if (callback) callback();
}, 1500);
} else {
addAnalysisLog('通路稳定!');
if (callback) setTimeout(callback, 500);
}
}
// 显示对话
function showDialog(dialogs, callback = null) {
dialogQueue = [...dialogs];
dialogCallback = callback;
showNextDialog();
}
function showNextDialog() {
if (dialogQueue.length === 0) {
document.getElementById('dialogBox').classList.remove('show');
if (dialogCallback) dialogCallback();
return;
}
const dialog = dialogQueue.shift();
document.getElementById('dialogSpeaker').textContent = dialog.speaker;
document.getElementById('dialogText').textContent = dialog.text;
document.getElementById('dialogBox').classList.add('show');
}
document.getElementById('dialogBox').addEventListener('click', showNextDialog);
// 更新阶段指示器
function updatePhase(phase) {
state.currentPhase = phase;
for (let i = 1; i <= 5; i++) {
const dot = document.getElementById('phase' + i);
dot.classList.remove('active', 'completed');
if (i < phase) dot.classList.add('completed');
if (i === phase) dot.classList.add('active');
}
}
// 显示模块
function showModule(module) {
// 隐藏所有视图
document.querySelectorAll('.diagnosis-view, .particle-game').forEach(el => {
el.classList.add('hidden');
});
// 更新按钮状态
document.querySelectorAll('.module-btn').forEach(btn => btn.classList.remove('active'));
if (module === 'voice') {
document.getElementById('voiceView').classList.remove('hidden');
document.getElementById('btnVoice').classList.add('active');
} else if (module === 'trigger') {
document.getElementById('triggerView').classList.remove('hidden');
document.getElementById('btnTrigger').classList.add('active');
} else if (module === 'processor') {
if (!state.processorUnlocked) {
if (state.voiceChecked && state.triggerChecked) {
unlockProcessor();
}
return;
}
document.getElementById('btnProcessor').classList.add('active');
// 显示游戏按钮
if (state.gameRound === 0) {
document.getElementById('btnGame1').style.display = 'block';
}
}
}
// 提取语音数据
function extractVoiceData() {
const btn = document.getElementById('extractBtn');
btn.disabled = true;
btn.textContent = '提取中...';
setTimeout(() => {
document.getElementById('voiceResult').classList.remove('hidden');
setTimeout(() => {
document.getElementById('voiceDetail').classList.remove('hidden');
state.voiceChecked = true;
document.getElementById('voiceStatus').textContent = '✓ 已提取(语调异常)';
document.getElementById('voiceStatus').classList.add('warning');
checkProcessorUnlock();
showDialog([
{ speaker: "医生", text: "奇怪...他说的是笑话,但语调好像不太对。" },
{ speaker: "医生", text: "先记下来,去看看情绪触发器。" }
]);
}, 500);
}, 1000);
}
// 采集波形
function captureWaveform() {
const btn = document.getElementById('captureBtn');
btn.disabled = true;
btn.textContent = '采集中...';
setTimeout(() => {
document.getElementById('waveformCard').classList.remove('hidden');
drawWaveform();
setTimeout(() => {
document.getElementById('emotionSelector').classList.remove('hidden');
btn.style.display = 'none';
}, 500);
}, 1000);
}
// 绘制波形
function drawWaveform() {
const canvas = document.getElementById('waveformCanvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.clientWidth;
canvas.height = 60;
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.beginPath();
for (let x = 0; x < canvas.width; x++) {
const y = 30 + Math.sin(x * 0.05) * 15 + Math.sin(x * 0.12) * 10 + Math.random() * 8;
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
// 选择情绪
function selectEmotion(element, emotion, match) {
document.querySelectorAll('.emotion-option').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
state.selectedEmotion = { emotion, match };
document.getElementById('confirmEmotionBtn').disabled = false;
}
// 确认情绪分析
function confirmEmotion() {
if (state.selectedEmotion.emotion === 'fear') {
document.getElementById('emotionResult').classList.remove('hidden');
state.triggerChecked = true;
document.getElementById('triggerStatus').textContent = '✓ 已分析(恐惧)';
document.getElementById('triggerStatus').classList.add('warning');
checkProcessorUnlock();
showDialog([
{ speaker: "医生", text: "原来如此...这明显是恐惧和焦虑的波形。" },
{ speaker: "医生", text: "但他说出来的是笑话...输入和输出完全对不上。" }
]);
} else {
showDialog([
{ speaker: "系统", text: "匹配度较低,请重新选择。" }
]);
}
}
// 检查是否可以解锁处理器
function checkProcessorUnlock() {
if (state.voiceChecked && state.triggerChecked && !state.processorUnlocked) {
document.getElementById('btnProcessor').classList.remove('locked');
document.getElementById('processorStatus').textContent = '可以进入';
document.getElementById('processorStatus').classList.remove('locked');
}
}
// 解锁处理器
function unlockProcessor() {
state.processorUnlocked = true;
document.getElementById('processorUnlockView').classList.remove('hidden');
updatePhase(2);
}
// 进入处理器
// 入场动画状态
let entranceState = {
phase: 'idle', // idle, generating, disrupting, analyzing, playing
allowInput: false,
entranceLoop: null
};
function enterProcessor() {
document.getElementById('processorUnlockView').classList.add('hidden');
// 重置状态
entranceState = { phase: 'generating', allowInput: false, entranceLoop: null };
state.gameRound = 1;
state.isGameRunning = false;
// 显示粒子游戏界面
document.querySelectorAll('.diagnosis-view').forEach(el => el.classList.add('hidden'));
document.getElementById('particleGame').classList.remove('hidden');
// 显示游戏UI(但先禁用输入)
document.querySelector('.game-overlay').style.display = '';
document.getElementById('gameInstruction').textContent = '[ 处理器检查中... ]';
document.getElementById('integrationFill').style.width = '0%';
document.getElementById('integrationPercent').textContent = '0%';
document.getElementById('resultScreen').classList.remove('show');
// 显示语言生成显示器
document.getElementById('languageDisplay').classList.remove('hidden');
document.getElementById('displayText').textContent = '';
document.getElementById('displayText').className = 'display-text generating';
showDialog([
{ speaker: "医生", text: "现在我知道了——输入是恐惧,输出却变成了笑话。" },
{ speaker: "医生", text: "问题一定出在中间的处理过程。让我观察一下..." }
], () => {
playEntranceSequence();
});
}
// 入场动画序列
function playEntranceSequence() {
const container = document.getElementById('particlesContainer');
const gameArea = document.getElementById('particleGame');
const config = gameConfigs[1];
const rect = gameArea.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const centerX = width / 2;
const centerY = height / 2;
container.innerHTML = '';
document.getElementById('connectionsSvg').innerHTML = '';
state.particles = [];
const targetChars = ['我', '害', '怕', '被'];
const nonTargetChars = ['哈', '笑', '话', '嘿', '呵', '好', '棒', '赞'];
const displayText = document.getElementById('displayText');
let generatedText = '';
// 阶段1:绿色粒子一个一个在中心生成
document.getElementById('gameInstruction').textContent = '[ 语言生成中... ]';
let greenIndex = 0;
const generateGreen = () => {
if (greenIndex >= config.targetCount) {
// 绿色生成完毕,开始聚合循环
startAggregationLoop();
return;
}
const i = greenIndex;
const angle = (Math.PI * 2 / config.targetCount) * i + Math.random() * 0.3;
const radius = 30 + Math.random() * 20;
const particle = {
id: state.particles.length,
isTarget: true,
x: centerX + Math.cos(angle) * radius - 30,
y: centerY + Math.sin(angle) * radius - 30,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
char: targetChars[i % targetChars.length]
};
const el = document.createElement('div');
el.className = 'particle target';
el.textContent = particle.char;
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
el.style.opacity = '0';
el.style.transform = 'scale(0.3)';
el.id = 'particle-' + particle.id;
container.appendChild(el);
state.particles.push(particle);
// 淡入动画
setTimeout(() => {
el.style.transition = 'opacity 0.3s, transform 0.3s';
el.style.opacity = '1';
el.style.transform = 'scale(1)';
}, 50);
// 更新显示器文字
generatedText += particle.char;
displayText.textContent = generatedText + '_';
greenIndex++;
setTimeout(generateGreen, 400);
};
// 开始生成绿色粒子
setTimeout(generateGreen, 500);
// 聚合循环:绿色粒子互相吸引
function startAggregationLoop() {
displayText.textContent = generatedText + '...';
entranceState.entranceLoop = setInterval(() => {
// 绿色粒子向中心聚合
state.particles.filter(p => p.isTarget).forEach(particle => {
const dx = centerX - 30 - particle.x;
const dy = centerY - 30 - particle.y;
const dist = Math.hypot(dx, dy);
if (dist > 10) {
particle.vx += (dx / dist) * 0.15;
particle.vy += (dy / dist) * 0.15;
}
// 更新位置
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= 0.95;
particle.vy *= 0.95;
const el = document.getElementById('particle-' + particle.id);
if (el) {
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
}
});
drawConnections();
}, 16);
// 1.5秒后,红色粒子涌入
setTimeout(startDisruption, 1500);
}
// 阶段2:红色粒子涌入打散
function startDisruption() {
entranceState.phase = 'disrupting';
document.getElementById('gameInstruction').innerHTML = '[ ⚠️ 检测到干扰信号!]';
displayText.className = 'display-text error';
// 创建红色粒子(从四周进入)
for (let i = 0; i < config.nonTargetCount; i++) {
const angle = (Math.PI * 2 / config.nonTargetCount) * i;
const startDist = Math.max(width, height) * 0.6;
const particle = {
id: state.particles.length,
isTarget: false,
x: centerX + Math.cos(angle) * startDist - 30,
y: centerY + Math.sin(angle) * startDist - 30,
vx: -Math.cos(angle) * 3, // 向中心冲
vy: -Math.sin(angle) * 3,
char: nonTargetChars[i % nonTargetChars.length]
};
const el = document.createElement('div');
el.className = 'particle non-target';
el.textContent = particle.char;
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
el.style.opacity = '0';
el.id = 'particle-' + particle.id;
container.appendChild(el);
state.particles.push(particle);
setTimeout(() => {
el.style.transition = 'opacity 0.2s';
el.style.opacity = '1';
}, i * 50);
}
// 更新显示器
setTimeout(() => {
displayText.textContent = '哈哈笑话...';
}, 800);
// 切换到干扰循环
clearInterval(entranceState.entranceLoop);
entranceState.entranceLoop = setInterval(() => {
const gameArea = document.getElementById('particleGame');
const rect = gameArea.getBoundingClientRect();
state.particles.forEach(particle => {
// 红色粒子向中心冲
if (!particle.isTarget) {
const dx = centerX - 30 - particle.x;
const dy = centerY - 30 - particle.y;
const dist = Math.hypot(dx, dy);
if (dist > 80) {
particle.vx += (dx / dist) * 0.2;
particle.vy += (dy / dist) * 0.2;
}
}
// 粒子之间的排斥力(红色排斥绿色)
state.particles.forEach(other => {
if (other.id === particle.id) return;
const dx = particle.x - other.x;
const dy = particle.y - other.y;
const dist = Math.hypot(dx, dy);
if (dist < 80 && dist > 1) {
const force = (80 - dist) / 80;
// 红色对绿色的排斥力更强
const multiplier = (!particle.isTarget && other.isTarget) ? 2.5 : 0.8;
particle.vx += (dx / dist) * force * multiplier;
particle.vy += (dy / dist) * force * multiplier;
}
});
// 更新位置
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= 0.96;
particle.vy *= 0.96;
// 边界
if (particle.x < 30) { particle.x = 30; particle.vx *= -0.5; }
if (particle.x > rect.width - 90) { particle.x = rect.width - 90; particle.vx *= -0.5; }
if (particle.y < 60) { particle.y = 60; particle.vy *= -0.5; }
if (particle.y > rect.height - 90) { particle.y = rect.height - 90; particle.vy *= -0.5; }
const el = document.getElementById('particle-' + particle.id);
if (el) {
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
}
});
drawConnections();
}, 16);
// 2秒后进入分析阶段
setTimeout(finishEntrance, 2000);
}
// 入场完成
function finishEntrance() {
clearInterval(entranceState.entranceLoop);
entranceState.entranceLoop = null;
document.getElementById('gameInstruction').textContent = '[ 干扰递质劫持了语言生成 ]';
showDialog([
{ speaker: "医生", text: "看到了...干扰递质打乱了正常的语言生成。" },
{ speaker: "医生", text: "让我分析一下这个竞争模式..." }
], () => {
entranceState.phase = 'analyzing';
// 隐藏显示器
document.getElementById('languageDisplay').classList.add('hidden');
runSignalAnalysis(() => {
showDialog([
{ speaker: "医生", text: "找到原因了。真实表达被幽默防御压制。" },
{ speaker: "医生", text: "我需要帮火山清理这些干扰,让真实的声音能够连接。" }
], () => {
// 解锁输入,开始游戏
entranceState.phase = 'playing';
entranceState.allowInput = true;
state.isGameRunning = true;
document.getElementById('gameInstruction').textContent = '左键排斥粒子 | 右键吸引粒子 | 目标:让绿色粒子相连';
document.getElementById('btnGame1').style.display = 'block';
// 开始正常游戏循环
requestAnimationFrame(gameLoop);
showHuoshanBubble(1);
});
});
});
}
}
// 开始粒子游戏
function startParticleGame(round) {
state.gameRound = round;
state.isGameRunning = true;
entranceState.allowInput = true;
entranceState.phase = 'playing';
const config = gameConfigs[round];
// 隐藏其他视图
document.querySelectorAll('.diagnosis-view').forEach(el => el.classList.add('hidden'));
document.getElementById('particleGame').classList.remove('hidden');
// 确保正常游戏UI显示
document.querySelector('.game-overlay').style.display = '';
// 更新UI
document.getElementById('gameInstruction').textContent = '左键排斥粒子 | 右键吸引粒子 | 目标:让绿色粒子相连';
updatePhase(3);
// 隐藏结果屏幕
document.getElementById('resultScreen').classList.remove('show');
// 初始化粒子(第2、3轮需要重新初始化)
initParticles(config);
// 开始游戏循环
requestAnimationFrame(gameLoop);
// 显示火山气泡
showHuoshanBubble(round);
}
// 显示火山气泡
function showHuoshanBubble(round) {
const bubble = document.getElementById('huoshanBubble');
const texts = {
1: "哈哈,你在做什么?那些东西很重要的!",
2: "等等...你确定要这样吗...",
3: "我...我来试试..."
};
bubble.textContent = texts[round];
bubble.classList.add('show');
setTimeout(() => {
bubble.classList.remove('show');
}, 3000);
}
// 初始化粒子
function initParticles(config) {
const container = document.getElementById('particlesContainer');
const gameArea = document.getElementById('particleGame');
container.innerHTML = '';
state.particles = [];
const width = gameArea.clientWidth;
const height = gameArea.clientHeight;
// 创建目标粒子(绿色 - 真实表达)
for (let i = 0; i < config.targetCount; i++) {
createParticle(true, width, height, container);
}
// 创建非目标粒子(红色 - 干扰/笑话)
for (let i = 0; i < config.nonTargetCount; i++) {
createParticle(false, width, height, container);
}
}
// 创建粒子
function createParticle(isTarget, maxWidth, maxHeight, container) {
const particle = {
id: state.particles.length,
isTarget: isTarget,
x: Math.random() * (maxWidth - 100) + 50,
y: Math.random() * (maxHeight - 150) + 75,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
char: isTarget ? ['真', '实', '表', '达', '我', '怕'][state.particles.filter(p => p.isTarget).length % 6] :
['哈', '笑', '话', '嘿', '呵'][Math.floor(Math.random() * 5)]
};
const el = document.createElement('div');
el.className = `particle ${isTarget ? 'target' : 'non-target'}`;
el.textContent = particle.char;
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
el.id = 'particle-' + particle.id;
container.appendChild(el);
state.particles.push(particle);
}
// 游戏循环
function gameLoop() {
if (!state.isGameRunning) return;
const gameArea = document.getElementById('particleGame');
const width = gameArea.clientWidth;
const height = gameArea.clientHeight;
// 更新粒子
updateNormalMode(width, height);
// 绘制连接线
drawConnections();
// 计算整合度
const integration = calculateIntegration();
document.getElementById('integrationFill').style.width = integration + '%';
document.getElementById('integrationPercent').textContent = Math.round(integration) + '%';
if (integration >= 100) {
completeGame();
}
requestAnimationFrame(gameLoop);
}
// 普通模式更新
function updateNormalMode(width, height) {
state.particles.forEach(particle => {
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.x < 30 || particle.x > width - 90) particle.vx *= -1;
if (particle.y < 60 || particle.y > height - 90) particle.vy *= -1;
// 鼠标交互(只有在允许输入时才响应)
if (state.isMouseDown && entranceState.allowInput) {
const dx = particle.x - state.mousePos.x;
const dy = particle.y - state.mousePos.y;
const dist = Math.hypot(dx, dy);
if (dist < 120 && dist > 0) {
const force = (120 - dist) / 120;
if (state.mouseButton === 0) {
particle.vx += (dx / dist) * force * 3;
particle.vy += (dy / dist) * force * 3;
} else if (state.mouseButton === 2) {
particle.vx -= (dx / dist) * force * 1.5;
particle.vy -= (dy / dist) * force * 1.5;
}
}
}
particle.vx *= 0.98;
particle.vy *= 0.98;
const el = document.getElementById('particle-' + particle.id);
if (el) {
el.style.left = particle.x + 'px';
el.style.top = particle.y + 'px';
}
});
}
// 绘制连接线
function drawConnections() {
const svg = document.getElementById('connectionsSvg');
svg.innerHTML = '';
const targets = state.particles.filter(p => p.isTarget);
for (let i = 0; i < targets.length; i++) {
for (let j = i + 1; j < targets.length; j++) {
const dist = Math.hypot(targets[i].x - targets[j].x, targets[i].y - targets[j].y);
if (dist < 120) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', targets[i].x + 30);
line.setAttribute('y1', targets[i].y + 30);
line.setAttribute('x2', targets[j].x + 30);
line.setAttribute('y2', targets[j].y + 30);
line.setAttribute('class', 'connection-line');
line.style.stroke = 'rgba(100,255,218,0.6)';
line.style.opacity = 1 - dist / 120;
svg.appendChild(line);
}
}
}
}
// 计算整合度
function calculateIntegration() {
const targets = state.particles.filter(p => p.isTarget);
const nonTargets = state.particles.filter(p => !p.isTarget);
if (targets.length < 2) return 100;
const connectionDist = 120; // 连接距离
// 检查目标粒子是否都相连(使用BFS检查连通性)
const visited = new Set();
const queue = [targets[0]];
visited.add(targets[0].id);
while (queue.length > 0) {
const current = queue.shift();
for (const other of targets) {
if (!visited.has(other.id)) {
const dist = Math.hypot(current.x - other.x, current.y - other.y);
if (dist < connectionDist) {
visited.add(other.id);
queue.push(other);
}
}
}
}
// 检查是否所有目标粒子都连通
const allConnected = visited.size === targets.length;
// 检查是否与非目标粒子分离
let separated = true;
for (const target of targets) {
for (const nonTarget of nonTargets) {
const dist = Math.hypot(target.x - nonTarget.x, target.y - nonTarget.y);
if (dist < connectionDist) {
separated = false;
break;
}
}
if (!separated) break;
}
// 计算整合度
// 基础分:连通的目标粒子比例
const connectivityScore = (visited.size / targets.length) * 70;
// 额外分:如果与干扰分离
const separationScore = separated ? 30 : 0;
const integration = connectivityScore + separationScore;
// 如果完全连通且分离,返回100
if (allConnected && separated) {
return 100;
}
return Math.min(99, integration);
}
// 完成游戏
function completeGame() {
state.isGameRunning = false;
const config = gameConfigs[state.gameRound];
document.getElementById('resultText').textContent = '连接成功!';
document.getElementById('resultDetail').textContent = `火山说出:"${config.targetSentence}..."`;
if (state.gameRound < 3) {
document.getElementById('resultBtn').textContent = '继续';
} else {
document.getElementById('resultBtn').textContent = '查看结果';
}
setTimeout(() => {
document.getElementById('resultScreen').classList.add('show');
}, 500);
}
// 处理结果按钮
function handleResultAction() {
document.getElementById('resultScreen').classList.remove('show');
if (state.gameRound === 1) {
// 第一次清理完成后,触发信号分析
document.getElementById('btnGame1').style.display = 'none';
document.getElementById('particleGame').classList.add('hidden');
document.getElementById('welcomeView').classList.remove('hidden');
showDialog([
{ speaker: "医生", text: "连上了...但是等等,怎么断了?" },
{ speaker: "医生", text: "有什么东西在把它拉回去...让我追踪一下信号来源。" }
], () => {
// 触发信号分析
runSignalAnalysis(() => {
showDialog([
{ speaker: "医生", text: "找到了!原来有两条竞争的通路。" },
{ speaker: "医生", text: "真实表达和幽默防御在争夺主导权。" },
{ speaker: "医生", text: "现在幽默防御占上风...但我们可以改变这个。" }
], () => {
document.getElementById('btnGame2').style.display = 'block';
showDialog([{ speaker: "医生", text: "点击左侧的「第二次清理」继续。" }]);
});
});
});
} else if (state.gameRound === 2) {
// 第二次清理完成
document.getElementById('btnGame2').style.display = 'none';
document.getElementById('particleGame').classList.add('hidden');
document.getElementById('welcomeView').classList.remove('hidden');
// 拔河条移动后回弹
tugofwarRebound(40, 70, () => {
showDialog([
{ speaker: "医生", text: "有进展了!但幽默防御还在反扑..." },
{ speaker: "医生", text: "火山,再来一次。这次试着说出更多。" },
{ speaker: "火山", text: "我...我试试..." }
], () => {
document.getElementById('btnGame3').style.display = 'block';
showDialog([{ speaker: "医生", text: "点击左侧的「第三次清理」继续。" }]);
});
});
} else {
// 第三次清理完成 - 胜利!
document.getElementById('particleGame').classList.add('hidden');
// 拔河条完全倒向真实表达
updateTugofwar(5);
addAnalysisLog('真实表达通路已建立!');
addAnalysisLog('防御机制解除');
const statusEl = document.getElementById('tugofwarStatus');
statusEl.innerHTML = '<span style="color: #4ade80;">✓ 真实表达通路稳定</span>';
setTimeout(() => {
document.getElementById('finalView').classList.remove('hidden');
updatePhase(5);
showDialog([
{ speaker: "医生", text: "你做到了。这次是你自己说出来的。" },
{ speaker: "火山", text: "...是我们一起。" }
]);
}, 1000);
}
}
// 鼠标事件
const gameArea = document.getElementById('particleGame');
gameArea.addEventListener('mousemove', (e) => {
const rect = gameArea.getBoundingClientRect();
state.mousePos.x = e.clientX - rect.left - 30;
state.mousePos.y = e.clientY - rect.top - 30;
});
gameArea.addEventListener('mousedown', (e) => {
state.isMouseDown = true;
state.mouseButton = e.button;
e.preventDefault();
});
gameArea.addEventListener('mouseup', () => {
state.isMouseDown = false;
});
gameArea.addEventListener('mouseleave', () => {
state.isMouseDown = false;
});
gameArea.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
// 窗口大小变化时重绘波形
window.addEventListener('resize', () => {
if (!document.getElementById('waveformCard').classList.contains('hidden')) {
drawWaveform();
}
});
</script>
</body>
</html>