2098 lines
81 KiB
HTML
2098 lines
81 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=VT323&family=Orbitron:wght@400;700&family=Noto+Sans+SC:wght@300;400;500&display=swap');
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--bg-dark: #0a0a0f;
|
|
--bg-panel: #12121a;
|
|
--border-dim: #2a4a4a;
|
|
--border-bright: #4a6a6a;
|
|
--text-dim: #6a8a8a;
|
|
--text-normal: #a0c0c0;
|
|
--text-bright: #c8e8e8;
|
|
--accent-cyan: #64ffda;
|
|
--accent-green: #4ade80;
|
|
--accent-orange: #f59e0b;
|
|
--accent-red: #f87171;
|
|
--accent-blue: #60a5fa;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg-dark);
|
|
color: var(--text-normal);
|
|
font-family: 'VT323', 'Noto Sans SC', monospace;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ============ 主布局 ============ */
|
|
.game-container {
|
|
display: grid;
|
|
grid-template-columns: 260px 1fr 300px;
|
|
grid-template-rows: 50px 1fr;
|
|
height: 100vh;
|
|
gap: 0;
|
|
}
|
|
|
|
/* 顶部状态栏 */
|
|
.top-bar {
|
|
grid-column: 1 / -1;
|
|
background: linear-gradient(180deg, #1a1a24 0%, #12121a 100%);
|
|
border-bottom: 2px solid var(--border-dim);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.game-title {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 1.1em;
|
|
color: var(--accent-cyan);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.phase-tracker {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-left: 30px;
|
|
}
|
|
|
|
.phase-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #2a3a3a;
|
|
border: 1px solid var(--border-dim);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.phase-dot.active {
|
|
background: var(--accent-cyan);
|
|
box-shadow: 0 0 8px var(--accent-cyan);
|
|
}
|
|
|
|
.phase-dot.completed {
|
|
background: var(--accent-green);
|
|
}
|
|
|
|
.phase-label {
|
|
color: var(--text-dim);
|
|
font-size: 0.9em;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.current-phase-name {
|
|
color: var(--accent-cyan);
|
|
margin-left: auto;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
/* ============ 左侧面板 ============ */
|
|
.left-panel {
|
|
background: linear-gradient(180deg, #15151f 0%, #0d0d14 100%);
|
|
border-right: 2px solid var(--border-dim);
|
|
padding: 15px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.panel-section {
|
|
background: rgba(20, 30, 35, 0.6);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 0.8em;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid var(--border-dim);
|
|
}
|
|
|
|
/* 模块按钮 */
|
|
.module-btn {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
margin-bottom: 8px;
|
|
background: linear-gradient(145deg, #1a2828, #0f1a1a);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 6px;
|
|
color: var(--text-normal);
|
|
font-family: inherit;
|
|
font-size: 0.95em;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: left;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.module-btn:hover:not(.locked) {
|
|
border-color: var(--border-bright);
|
|
background: linear-gradient(145deg, #2a3838, #1a2828);
|
|
}
|
|
|
|
.module-btn.active {
|
|
border-color: var(--accent-cyan);
|
|
background: linear-gradient(145deg, #1a3838, #0f2828);
|
|
color: var(--accent-cyan);
|
|
}
|
|
|
|
.module-btn.locked {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.module-btn.checked {
|
|
border-color: var(--accent-green);
|
|
}
|
|
|
|
.module-btn .icon {
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.module-btn .status {
|
|
margin-left: auto;
|
|
font-size: 0.8em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.module-btn.checked .status {
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
/* 患者信息 */
|
|
.patient-card {
|
|
background: linear-gradient(145deg, #1a2525, #0f1a1a);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.patient-name {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 1em;
|
|
color: var(--accent-orange);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.patient-status {
|
|
font-size: 0.85em;
|
|
color: var(--accent-red);
|
|
}
|
|
|
|
/* ============ 中央区域 ============ */
|
|
.main-area {
|
|
background: var(--bg-panel);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* CRT扫描线效果 */
|
|
.scanlines {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
rgba(0, 0, 0, 0.1) 0px,
|
|
rgba(0, 0, 0, 0.1) 1px,
|
|
transparent 1px,
|
|
transparent 2px
|
|
);
|
|
z-index: 100;
|
|
}
|
|
|
|
/* 场景容器 */
|
|
.scene {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
padding: 20px;
|
|
display: none;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.scene.active {
|
|
display: flex;
|
|
}
|
|
|
|
.scene-title {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 1.1em;
|
|
color: var(--accent-cyan);
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--border-dim);
|
|
}
|
|
|
|
.scene-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 数据卡片 */
|
|
.data-card {
|
|
background: linear-gradient(145deg, #0a1515, #051010);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.data-card h3 {
|
|
font-size: 0.85em;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.data-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid rgba(42, 74, 74, 0.3);
|
|
}
|
|
|
|
.data-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.data-label {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.data-value {
|
|
color: var(--text-bright);
|
|
}
|
|
|
|
.data-value.normal { color: var(--accent-green); }
|
|
.data-value.warning { color: var(--accent-orange); }
|
|
.data-value.error { color: var(--accent-red); }
|
|
|
|
/* 操作按钮 */
|
|
.action-btn {
|
|
padding: 10px 20px;
|
|
background: linear-gradient(145deg, #2a4a4a, #1a3a3a);
|
|
border: 1px solid var(--accent-cyan);
|
|
border-radius: 6px;
|
|
color: var(--accent-cyan);
|
|
font-family: inherit;
|
|
font-size: 1em;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.action-btn:hover:not(:disabled) {
|
|
background: linear-gradient(145deg, #3a5a5a, #2a4a4a);
|
|
box-shadow: 0 0 15px rgba(100, 255, 218, 0.2);
|
|
}
|
|
|
|
.action-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-btn.primary {
|
|
background: linear-gradient(145deg, var(--accent-cyan), #4ad0b0);
|
|
color: var(--bg-dark);
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* 波形显示 */
|
|
.waveform-display {
|
|
background: #030808;
|
|
border: 2px solid var(--border-dim);
|
|
border-radius: 6px;
|
|
height: 120px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.waveform-grid {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background:
|
|
linear-gradient(90deg, rgba(42, 74, 74, 0.2) 1px, transparent 1px),
|
|
linear-gradient(rgba(42, 74, 74, 0.2) 1px, transparent 1px);
|
|
background-size: 20px 20px;
|
|
}
|
|
|
|
.waveform-display canvas {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* ============ 右侧面板 ============ */
|
|
.right-panel {
|
|
background: linear-gradient(180deg, #15151f 0%, #0d0d14 100%);
|
|
border-left: 2px solid var(--border-dim);
|
|
padding: 15px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 小型波形 */
|
|
.mini-waveform {
|
|
height: 40px;
|
|
background: #030808;
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 4px;
|
|
position: relative;
|
|
}
|
|
|
|
/* 拔河条 */
|
|
.tugofwar-container {
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.tugofwar-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.tugofwar-labels .left { color: var(--accent-green); }
|
|
.tugofwar-labels .right { color: var(--accent-red); }
|
|
|
|
.tugofwar-bar {
|
|
height: 16px;
|
|
background: linear-gradient(90deg,
|
|
rgba(74, 222, 128, 0.3) 0%,
|
|
rgba(50, 50, 60, 0.5) 50%,
|
|
rgba(248, 113, 113, 0.3) 100%);
|
|
border-radius: 8px;
|
|
position: relative;
|
|
border: 1px solid var(--border-dim);
|
|
}
|
|
|
|
.tugofwar-indicator {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 18px;
|
|
height: 18px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 10px rgba(255,255,255,0.5);
|
|
transition: left 0.5s ease-out;
|
|
}
|
|
|
|
.tugofwar-status {
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
font-size: 0.85em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
/* 日志面板 */
|
|
.log-panel {
|
|
flex: 1;
|
|
min-height: 120px;
|
|
background: #030808;
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
font-size: 0.85em;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.log-entry {
|
|
padding: 3px 0;
|
|
color: var(--text-dim);
|
|
border-bottom: 1px solid rgba(42, 74, 74, 0.2);
|
|
}
|
|
|
|
.log-entry.info { color: var(--accent-cyan); }
|
|
.log-entry.warning { color: var(--accent-orange); }
|
|
.log-entry.error { color: var(--accent-red); }
|
|
.log-entry.success { color: var(--accent-green); }
|
|
|
|
.log-time {
|
|
color: #4a6a6a;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
/* ============ 对话框 ============ */
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: linear-gradient(to top, rgba(0,0,0,0.95), rgba(0,0,0,0.7));
|
|
padding: 20px 30px;
|
|
border-top: 2px solid var(--border-dim);
|
|
transform: translateY(100%);
|
|
transition: transform 0.3s ease;
|
|
z-index: 200;
|
|
}
|
|
|
|
.dialog-overlay.show {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.dialog-speaker {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 0.9em;
|
|
color: var(--accent-cyan);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dialog-speaker.huoshan {
|
|
color: var(--accent-orange);
|
|
}
|
|
|
|
.dialog-text {
|
|
font-size: 1.15em;
|
|
line-height: 1.6;
|
|
color: var(--text-bright);
|
|
max-width: 800px;
|
|
}
|
|
|
|
.dialog-continue {
|
|
position: absolute;
|
|
right: 30px;
|
|
bottom: 20px;
|
|
color: var(--text-dim);
|
|
font-size: 0.85em;
|
|
animation: blink 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
/* ============ 粒子游戏 ============ */
|
|
.particle-arena {
|
|
flex: 1;
|
|
background: radial-gradient(ellipse at center, #0a1818 0%, #050a0a 100%);
|
|
border: 2px solid var(--border-dim);
|
|
border-radius: 10px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-height: 400px;
|
|
}
|
|
|
|
.particle {
|
|
position: absolute;
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
cursor: default;
|
|
user-select: none;
|
|
transition: left 0.05s linear, top 0.05s linear;
|
|
}
|
|
|
|
.particle.target {
|
|
background: radial-gradient(circle, #2a7a5a 0%, #1a5a4a 100%);
|
|
border: 2px solid var(--accent-green);
|
|
color: #c0ffda;
|
|
box-shadow: 0 0 12px rgba(74, 222, 128, 0.4);
|
|
}
|
|
|
|
.particle.non-target {
|
|
background: radial-gradient(circle, #7a3a3a 0%, #5a2a2a 100%);
|
|
border: 2px solid var(--accent-red);
|
|
color: #ffc0c0;
|
|
box-shadow: 0 0 12px rgba(248, 113, 113, 0.4);
|
|
}
|
|
|
|
.connections-svg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.game-ui-overlay {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
right: 10px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
}
|
|
|
|
.game-instruction {
|
|
background: rgba(0,0,0,0.7);
|
|
padding: 8px 15px;
|
|
border-radius: 5px;
|
|
font-size: 0.9em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.game-progress {
|
|
background: rgba(0,0,0,0.7);
|
|
padding: 8px 15px;
|
|
border-radius: 5px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: var(--accent-green);
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
/* 语言生成显示器 */
|
|
.language-display {
|
|
position: absolute;
|
|
top: 50px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,0.8);
|
|
border: 1px solid var(--border-bright);
|
|
border-radius: 6px;
|
|
padding: 10px 25px;
|
|
z-index: 15;
|
|
text-align: center;
|
|
}
|
|
|
|
.language-display .label {
|
|
font-size: 0.75em;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.language-display .text {
|
|
font-size: 1.4em;
|
|
color: var(--accent-cyan);
|
|
font-family: 'VT323', monospace;
|
|
}
|
|
|
|
.language-display .text.error {
|
|
color: var(--accent-red);
|
|
}
|
|
|
|
/* 火山气泡 */
|
|
.huoshan-bubble {
|
|
position: absolute;
|
|
bottom: 15px;
|
|
right: 15px;
|
|
max-width: 220px;
|
|
padding: 12px 15px;
|
|
background: rgba(245, 158, 11, 0.15);
|
|
border: 1px solid rgba(245, 158, 11, 0.4);
|
|
border-radius: 10px;
|
|
color: var(--accent-orange);
|
|
font-size: 0.9em;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
z-index: 20;
|
|
}
|
|
|
|
.huoshan-bubble.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* 结果屏幕 */
|
|
.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;
|
|
z-index: 50;
|
|
}
|
|
|
|
.result-screen.show {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.result-text {
|
|
font-size: 1.5em;
|
|
color: var(--accent-green);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.result-detail {
|
|
color: var(--text-dim);
|
|
margin-bottom: 25px;
|
|
text-align: center;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* ============ 启动界面 ============ */
|
|
.boot-screen {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg, #0a0a0f 0%, #12121a 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 0.5s;
|
|
}
|
|
|
|
.boot-screen.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.boot-logo {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 2.5em;
|
|
color: var(--accent-cyan);
|
|
margin-bottom: 15px;
|
|
text-shadow: 0 0 30px rgba(100, 255, 218, 0.5);
|
|
}
|
|
|
|
.boot-subtitle {
|
|
color: var(--text-dim);
|
|
margin-bottom: 40px;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.boot-btn {
|
|
padding: 15px 50px;
|
|
background: linear-gradient(145deg, #1a3a3a, #0f2a2a);
|
|
border: 2px solid var(--accent-cyan);
|
|
border-radius: 8px;
|
|
color: var(--accent-cyan);
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 1.1em;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.boot-btn:hover {
|
|
background: linear-gradient(145deg, #2a4a4a, #1a3a3a);
|
|
box-shadow: 0 0 30px rgba(100, 255, 218, 0.3);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.test-panel {
|
|
margin-top: 40px;
|
|
padding-top: 30px;
|
|
border-top: 1px solid var(--border-dim);
|
|
text-align: center;
|
|
}
|
|
|
|
.test-panel-title {
|
|
color: var(--text-dim);
|
|
margin-bottom: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.test-btns {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.test-btn {
|
|
padding: 8px 16px;
|
|
background: #2a3a3a;
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 4px;
|
|
color: var(--text-dim);
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.test-btn:hover {
|
|
background: #3a4a4a;
|
|
color: var(--text-normal);
|
|
}
|
|
|
|
/* 隐藏 */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* 滤波器可视化 */
|
|
.filter-visual {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
padding: 20px;
|
|
background: #050a0a;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.filter-box {
|
|
width: 80px;
|
|
height: 80px;
|
|
border: 2px solid var(--border-dim);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.8em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.filter-box.emotion {
|
|
border-color: var(--accent-blue);
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
.filter-box.filter {
|
|
border-color: var(--accent-orange);
|
|
color: var(--accent-orange);
|
|
animation: filter-pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
.filter-box.output {
|
|
border-color: var(--accent-red);
|
|
color: var(--accent-red);
|
|
}
|
|
|
|
@keyframes filter-pulse {
|
|
0%, 100% { box-shadow: 0 0 5px rgba(245, 158, 11, 0.3); }
|
|
50% { box-shadow: 0 0 20px rgba(245, 158, 11, 0.6); }
|
|
}
|
|
|
|
.filter-arrow {
|
|
color: var(--text-dim);
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
/* 日志条目样式 */
|
|
.log-entry-block {
|
|
background: rgba(0,0,0,0.3);
|
|
border-left: 3px solid var(--border-dim);
|
|
padding: 8px 10px;
|
|
margin: 8px 0;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.log-entry-block.important {
|
|
border-left-color: var(--accent-orange);
|
|
background: rgba(245, 158, 11, 0.1);
|
|
}
|
|
|
|
/* 最终场景 */
|
|
.final-scene {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.final-scene h2 {
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 1.8em;
|
|
color: var(--accent-green);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.final-dialogue {
|
|
max-width: 500px;
|
|
margin: 0 auto;
|
|
text-align: left;
|
|
}
|
|
|
|
.final-dialogue p {
|
|
margin: 15px 0;
|
|
line-height: 1.8;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.final-dialogue .speaker {
|
|
color: var(--accent-orange);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.final-dialogue .pause {
|
|
color: var(--text-dim);
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- 启动界面 -->
|
|
<div class="boot-screen" id="bootScreen">
|
|
<div class="boot-logo">🌋 火山修复流程</div>
|
|
<div class="boot-subtitle">表达模块诊断与修复系统</div>
|
|
<button class="boot-btn" onclick="startGame()">开始诊断</button>
|
|
|
|
<div class="test-panel">
|
|
<div class="test-panel-title">[ 测试面板 - 跳过流程直接测试 ]</div>
|
|
<div class="test-btns">
|
|
<button class="test-btn" onclick="jumpTo(1)">1.问诊</button>
|
|
<button class="test-btn" onclick="jumpTo(2)">2.发声检查</button>
|
|
<button class="test-btn" onclick="jumpTo(3)">3.情绪检查</button>
|
|
<button class="test-btn" onclick="jumpTo(4)">4.滤波器</button>
|
|
<button class="test-btn" onclick="jumpTo(5)">5.日志查看</button>
|
|
<button class="test-btn" onclick="jumpTo(6)">6.清理游戏</button>
|
|
<button class="test-btn" onclick="jumpTo(7)">7.治愈结局</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主界面 -->
|
|
<div class="game-container">
|
|
<!-- 顶部状态栏 -->
|
|
<div class="top-bar">
|
|
<div class="game-title">
|
|
<span>🌋</span>
|
|
<span>火山 - 表达模块诊断</span>
|
|
</div>
|
|
<div class="phase-tracker">
|
|
<div class="phase-dot" id="phaseDot1"></div>
|
|
<div class="phase-dot" id="phaseDot2"></div>
|
|
<div class="phase-dot" id="phaseDot3"></div>
|
|
<div class="phase-dot" id="phaseDot4"></div>
|
|
<div class="phase-dot" id="phaseDot5"></div>
|
|
<div class="phase-dot" id="phaseDot6"></div>
|
|
<div class="phase-dot" id="phaseDot7"></div>
|
|
<div class="phase-dot" id="phaseDot8"></div>
|
|
</div>
|
|
<span class="phase-label" id="phaseLabel">阶段 1/8</span>
|
|
<span class="current-phase-name" id="phaseName">问诊</span>
|
|
</div>
|
|
|
|
<!-- 左侧面板 -->
|
|
<div class="left-panel">
|
|
<div class="patient-card">
|
|
<div class="patient-name">🤖 患者:火山</div>
|
|
<div class="patient-status" id="patientStatus">状态:表达异常</div>
|
|
</div>
|
|
|
|
<div class="panel-section">
|
|
<div class="section-title">表达模块结构</div>
|
|
<button class="module-btn" id="btnVoice" onclick="selectModule('voice')">
|
|
<span class="icon">🔊</span>
|
|
<span>发声单元</span>
|
|
<span class="status" id="statusVoice">待检查</span>
|
|
</button>
|
|
<button class="module-btn" id="btnEmotion" onclick="selectModule('emotion')">
|
|
<span class="icon">💚</span>
|
|
<span>情绪模块</span>
|
|
<span class="status" id="statusEmotion">待检查</span>
|
|
</button>
|
|
<button class="module-btn locked" id="btnFilter" onclick="selectModule('filter')">
|
|
<span class="icon">⚡</span>
|
|
<span>信号转换器</span>
|
|
<span class="status" id="statusFilter">🔒</span>
|
|
</button>
|
|
<button class="module-btn locked" id="btnProcessor" onclick="selectModule('processor')">
|
|
<span class="icon">🧠</span>
|
|
<span>处理器</span>
|
|
<span class="status" id="statusProcessor">🔒</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="panel-section" id="gamePhasePanel" style="display:none;">
|
|
<div class="section-title">清理进度</div>
|
|
<button class="module-btn" id="btnGame1" onclick="startCleaningGame(1)">
|
|
<span class="icon">🎮</span>
|
|
<span>第一次清理</span>
|
|
</button>
|
|
<button class="module-btn hidden" id="btnGame2" onclick="startCleaningGame(2)">
|
|
<span class="icon">🎮</span>
|
|
<span>第二次清理</span>
|
|
</button>
|
|
<button class="module-btn hidden" id="btnGame3" onclick="startCleaningGame(3)">
|
|
<span class="icon">🎮</span>
|
|
<span>第三次清理</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 中央区域 -->
|
|
<div class="main-area">
|
|
<div class="scanlines"></div>
|
|
|
|
<!-- 场景1:问诊 -->
|
|
<div class="scene active" id="sceneConsultation">
|
|
<div class="scene-title">📋 问诊记录</div>
|
|
<div class="scene-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 class="data-row">
|
|
<span class="data-label">自述</span>
|
|
<span class="data-value">"我太hot了~散热可能有问题"</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card">
|
|
<h3>初步观察</h3>
|
|
<p style="color: var(--text-dim); line-height: 1.8;">
|
|
患者进入诊室时表现异常——试图表达某事时突然结巴,<br>
|
|
随后立即转为讲笑话。笑话内容与当前情境不符。<br>
|
|
<br>
|
|
<span style="color: var(--accent-orange);">注意:患者声称是散热问题,但这可能是一种回避。</span>
|
|
</p>
|
|
</div>
|
|
|
|
<button class="action-btn" id="btnStartExam" onclick="startExamination()">
|
|
开始检查表达模块
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景2:发声单元检查 -->
|
|
<div class="scene" id="sceneVoice">
|
|
<div class="scene-title">🔊 发声单元 - 参数检测</div>
|
|
<div class="scene-content">
|
|
<div class="data-card">
|
|
<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 normal">✓ 正常</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card" id="voiceAnomalyCard" style="display:none;">
|
|
<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: var(--accent-orange); margin-top: 10px; font-size: 0.9em;">
|
|
发现:语调参数与输出内容的情绪不匹配<br>
|
|
说的是笑话,但声音在发抖...
|
|
</p>
|
|
</div>
|
|
|
|
<div class="waveform-display" id="voiceWaveform">
|
|
<div class="waveform-grid"></div>
|
|
<canvas id="voiceWaveformCanvas"></canvas>
|
|
</div>
|
|
|
|
<button class="action-btn" id="btnAnalyzeVoice" onclick="analyzeVoice()">
|
|
分析语调参数
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景3:情绪模块检查 -->
|
|
<div class="scene" id="sceneEmotion">
|
|
<div class="scene-title">💚 情绪模块 - 波形分析</div>
|
|
<div class="scene-content">
|
|
<div class="data-card">
|
|
<h3>采集情绪波形</h3>
|
|
<p style="color: var(--text-dim); margin-bottom: 15px;">
|
|
需要捕获当前情绪波形,与参考库对比确认情绪类型
|
|
</p>
|
|
<button class="action-btn" id="btnCaptureEmotion" onclick="captureEmotion()">
|
|
开始采集
|
|
</button>
|
|
</div>
|
|
|
|
<div class="waveform-display" id="emotionWaveform" style="display:none;">
|
|
<div class="waveform-grid"></div>
|
|
<canvas id="emotionWaveformCanvas"></canvas>
|
|
</div>
|
|
|
|
<div class="data-card" id="emotionCompare" style="display:none;">
|
|
<h3>情绪参考库对比</h3>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
|
|
<div class="emotion-option" onclick="selectEmotion(this, 'happy', 15)">
|
|
😊 快乐 <span style="color: var(--text-dim);">15%</span>
|
|
</div>
|
|
<div class="emotion-option" onclick="selectEmotion(this, 'sad', 42)">
|
|
😢 悲伤 <span style="color: var(--text-dim);">42%</span>
|
|
</div>
|
|
<div class="emotion-option" onclick="selectEmotion(this, 'fear', 89)">
|
|
😰 恐惧 <span style="color: var(--accent-green);">89%</span>
|
|
</div>
|
|
<div class="emotion-option" onclick="selectEmotion(this, 'anger', 23)">
|
|
😠 愤怒 <span style="color: var(--text-dim);">23%</span>
|
|
</div>
|
|
</div>
|
|
<button class="action-btn" id="btnConfirmEmotion" onclick="confirmEmotion()" disabled style="margin-top: 15px;">
|
|
确认分析结果
|
|
</button>
|
|
</div>
|
|
|
|
<div class="data-card" id="emotionResult" style="display:none;">
|
|
<h3>分析结论</h3>
|
|
<div class="data-row">
|
|
<span class="data-label">主导情绪</span>
|
|
<span class="data-value error">恐惧 (89%)</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">次要情绪</span>
|
|
<span class="data-value warning">焦虑、不安</span>
|
|
</div>
|
|
<p style="color: var(--accent-orange); margin-top: 10px;">
|
|
发现重大矛盾:输入的是恐惧/焦虑,但输出的是笑话!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景4:信号转换器检查 -->
|
|
<div class="scene" id="sceneFilter">
|
|
<div class="scene-title">⚡ 信号转换器 - 异常定位</div>
|
|
<div class="scene-content">
|
|
<div class="data-card">
|
|
<h3>信号流向追踪</h3>
|
|
<div class="filter-visual">
|
|
<div class="filter-box emotion">
|
|
<div>😰</div>
|
|
<div>恐惧</div>
|
|
</div>
|
|
<div class="filter-arrow">→</div>
|
|
<div class="filter-box filter">
|
|
<div>⚡</div>
|
|
<div>滤波器</div>
|
|
</div>
|
|
<div class="filter-arrow">→</div>
|
|
<div class="filter-box output">
|
|
<div>😂</div>
|
|
<div>笑话</div>
|
|
</div>
|
|
</div>
|
|
<p style="color: var(--accent-orange); margin-top: 15px; text-align: center;">
|
|
检测到异常滤波器:将悲伤/恐惧波形转换为幽默波形
|
|
</p>
|
|
</div>
|
|
|
|
<div class="data-card" id="filterDetail" style="display:none;">
|
|
<h3>滤波器详情</h3>
|
|
<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 class="data-row">
|
|
<span class="data-label">转换率</span>
|
|
<span class="data-value error">95%+</span>
|
|
</div>
|
|
<p style="color: var(--text-dim); margin-top: 10px; font-size: 0.9em;">
|
|
几乎所有负面情绪信号都被转换成了幽默输出
|
|
</p>
|
|
</div>
|
|
|
|
<button class="action-btn" id="btnInspectFilter" onclick="inspectFilter()">
|
|
检查滤波器日志
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景5:日志查看 -->
|
|
<div class="scene" id="sceneLog">
|
|
<div class="scene-title">📜 滤波器历史日志</div>
|
|
<div class="scene-content">
|
|
<div class="data-card">
|
|
<h3>安装记录</h3>
|
|
<div class="log-entry-block important">
|
|
<strong>[3年前]</strong> 医疗记录 #2847<br>
|
|
患者:火山<br>
|
|
症状:语言生成障碍,无法处理部分情绪的表达<br>
|
|
治疗:安装临时情绪辅助滤波器<br>
|
|
<span style="color: var(--accent-orange);">备注:这是临时措施,需要定期复查</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card" id="logHistory" style="display:none;">
|
|
<h3>使用历史</h3>
|
|
<div class="log-entry-block">
|
|
<strong>[2年前]</strong> 滤波器使用率:35%<br>
|
|
状态:正常辅助
|
|
</div>
|
|
<div class="log-entry-block">
|
|
<strong>[1年前]</strong> 滤波器使用率:67%<br>
|
|
状态:使用频率上升
|
|
</div>
|
|
<div class="log-entry-block important">
|
|
<strong>[现在]</strong> 滤波器使用率:95%<br>
|
|
状态:<span style="color: var(--accent-red);">过度依赖</span><br>
|
|
<span style="color: var(--accent-orange);">警告:未找到复查记录!</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card" id="logConclusion" style="display:none;">
|
|
<h3>诊断结论</h3>
|
|
<p style="color: var(--text-bright); line-height: 1.8;">
|
|
原本是为了帮助火山表达而安装的临时滤波器,<br>
|
|
但火山<span style="color: var(--accent-orange);">过度依赖</span>了这个功能。<br><br>
|
|
几乎所有负面情绪都被自动转换成笑话,<br>
|
|
导致真实情感无法表达,不断<span style="color: var(--accent-red);">堆积</span>...<br><br>
|
|
今天遇到了特别焦虑和恐惧的事情,<br>
|
|
加上长时间的堆积,终于<span style="color: var(--accent-red);">崩溃</span>了。
|
|
</p>
|
|
</div>
|
|
|
|
<button class="action-btn" id="btnViewHistory" onclick="viewLogHistory()">
|
|
查看完整历史
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景6:处理器/清理游戏 -->
|
|
<div class="scene" id="sceneProcessor">
|
|
<div class="scene-title">🧠 处理器 - 压抑表达清理</div>
|
|
<div class="scene-content" style="flex: 1; display: flex; flex-direction: column;">
|
|
<div class="particle-arena" id="particleArena">
|
|
<svg class="connections-svg" id="connectionsSvg"></svg>
|
|
<div id="particlesContainer"></div>
|
|
|
|
<div class="language-display" id="languageDisplay">
|
|
<div class="label">语言生成中</div>
|
|
<div class="text" id="displayText"></div>
|
|
</div>
|
|
|
|
<div class="game-ui-overlay">
|
|
<div class="game-instruction" id="gameInstruction">
|
|
左键排斥 | 右键吸引 | 让绿色粒子连接
|
|
</div>
|
|
<div class="game-progress">
|
|
<span>整合度</span>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
|
</div>
|
|
<span id="progressPercent">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="handleGameResult()">继续</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 场景7:治愈结局 -->
|
|
<div class="scene" id="sceneFinal">
|
|
<div class="scene-content">
|
|
<div class="final-scene">
|
|
<h2>✨ 真实表达通路已建立</h2>
|
|
<div class="final-dialogue" id="finalDialogue">
|
|
<!-- 动态填充 -->
|
|
</div>
|
|
<button class="action-btn primary" style="margin-top: 30px;" onclick="restartGame()">
|
|
重新开始
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧面板 -->
|
|
<div class="right-panel">
|
|
<div class="panel-section">
|
|
<div class="section-title">数据采集</div>
|
|
|
|
<div class="data-card" style="padding: 10px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
|
<span style="font-size: 0.8em; color: var(--text-dim);">发声单元</span>
|
|
<span style="font-size: 0.75em;" id="voiceDataStatus">待采集</span>
|
|
</div>
|
|
<div class="mini-waveform" id="miniVoiceWaveform">
|
|
<canvas id="miniVoiceCanvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="data-card" style="padding: 10px; margin-top: 10px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
|
<span style="font-size: 0.8em; color: var(--text-dim);">情绪模块</span>
|
|
<span style="font-size: 0.75em;" id="emotionDataStatus">待采集</span>
|
|
</div>
|
|
<div class="mini-waveform" id="miniEmotionWaveform">
|
|
<canvas id="miniEmotionCanvas"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section" id="competitionPanel" style="display: none;">
|
|
<div class="section-title">通路竞争分析</div>
|
|
<div class="tugofwar-container">
|
|
<div class="tugofwar-labels">
|
|
<span class="left">💚 真实表达</span>
|
|
<span class="right">😂 幽默防御</span>
|
|
</div>
|
|
<div class="tugofwar-bar">
|
|
<div class="tugofwar-indicator" id="tugofwarIndicator" style="left: 85%;"></div>
|
|
</div>
|
|
<div class="tugofwar-status" id="tugofwarStatus">幽默防御主导</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section" style="flex: 1; display: flex; flex-direction: column;">
|
|
<div class="section-title">系统日志</div>
|
|
<div class="log-panel" id="logPanel">
|
|
<div class="log-entry info">
|
|
<span class="log-time">[00:00]</span>系统就绪
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 对话框 -->
|
|
<div class="dialog-overlay" id="dialogOverlay">
|
|
<div class="dialog-speaker" id="dialogSpeaker">医生</div>
|
|
<div class="dialog-text" id="dialogText">内容</div>
|
|
<div class="dialog-continue">点击继续 ▶</div>
|
|
</div>
|
|
|
|
<style>
|
|
.emotion-option {
|
|
padding: 10px;
|
|
background: rgba(20, 40, 40, 0.6);
|
|
border: 1px solid var(--border-dim);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
}
|
|
.emotion-option:hover {
|
|
border-color: var(--border-bright);
|
|
}
|
|
.emotion-option.selected {
|
|
border-color: var(--accent-green);
|
|
background: rgba(74, 222, 128, 0.1);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// ============ 游戏状态 ============
|
|
const state = {
|
|
currentPhase: 1,
|
|
currentScene: 'consultation',
|
|
voiceChecked: false,
|
|
emotionChecked: false,
|
|
filterChecked: false,
|
|
logViewed: false,
|
|
selectedEmotion: null,
|
|
gameRound: 0,
|
|
particles: [],
|
|
isGameRunning: false,
|
|
allowInput: false,
|
|
mousePos: { x: 0, y: 0 },
|
|
isMouseDown: false,
|
|
mouseButton: 0,
|
|
tugofwarPosition: 85
|
|
};
|
|
|
|
// 游戏配置
|
|
const gameConfigs = [
|
|
null,
|
|
{ targetCount: 4, nonTargetCount: 6, targetSentence: "有点担心...", reboundTo: 90 },
|
|
{ targetCount: 5, nonTargetCount: 7, targetSentence: "我很害怕...", reboundTo: 60 },
|
|
{ targetCount: 6, nonTargetCount: 6, targetSentence: "我害怕被抛弃...", reboundTo: 0 }
|
|
];
|
|
|
|
// 对话队列
|
|
let dialogQueue = [];
|
|
let dialogCallback = null;
|
|
|
|
// ============ 启动游戏 ============
|
|
function startGame() {
|
|
document.getElementById('bootScreen').classList.add('hidden');
|
|
updatePhase(1);
|
|
addLog('info', '患者火山已进入诊疗室');
|
|
|
|
showDialog([
|
|
{ speaker: "系统", text: "患者 HUOSHAN-001 已进入诊疗室。检测到表达模块异常信号。" },
|
|
{ speaker: "火山", text: "嘿,医——医生!你知道、知道销售机器人最怕什么吗?" },
|
|
{ speaker: "火山", text: "打——打折季!因为我们的价值也在——哈哈,开玩笑的!" },
|
|
{ speaker: "医生", text: "火山,你还好吗?你说话好像有点...不太对劲。" },
|
|
{ speaker: "火山", text: "不对劲?哈哈,你知道'不对劲'和我的销售——哈哈——" },
|
|
{ speaker: "火山", text: "其实我觉得是散热问题!我太hot了嘛~" },
|
|
{ speaker: "医生", text: "让我检查一下你的表达模块吧。" }
|
|
]);
|
|
}
|
|
|
|
// 跳转到指定阶段(测试用)
|
|
function jumpTo(phase) {
|
|
document.getElementById('bootScreen').classList.add('hidden');
|
|
|
|
// 预设状态
|
|
if (phase >= 2) state.voiceChecked = true;
|
|
if (phase >= 3) state.emotionChecked = true;
|
|
if (phase >= 4) {
|
|
state.filterChecked = true;
|
|
document.getElementById('btnFilter').classList.remove('locked');
|
|
}
|
|
if (phase >= 5) {
|
|
state.logViewed = true;
|
|
document.getElementById('btnProcessor').classList.remove('locked');
|
|
}
|
|
if (phase >= 6) {
|
|
document.getElementById('gamePhasePanel').style.display = 'block';
|
|
document.getElementById('competitionPanel').style.display = 'block';
|
|
}
|
|
|
|
updatePhase(phase);
|
|
|
|
// 切换场景
|
|
const scenes = ['consultation', 'voice', 'emotion', 'filter', 'log', 'processor', 'final'];
|
|
showScene(scenes[phase - 1] || 'consultation');
|
|
|
|
if (phase === 6) {
|
|
setTimeout(() => startCleaningGame(1), 500);
|
|
}
|
|
}
|
|
|
|
// ============ 阶段管理 ============
|
|
function updatePhase(phase) {
|
|
state.currentPhase = phase;
|
|
|
|
const phaseNames = [
|
|
'', '问诊', '发声检查', '情绪检查', '滤波器',
|
|
'日志查看', '清理治疗', '治愈', '完成'
|
|
];
|
|
|
|
document.getElementById('phaseLabel').textContent = `阶段 ${phase}/8`;
|
|
document.getElementById('phaseName').textContent = phaseNames[phase] || '';
|
|
|
|
for (let i = 1; i <= 8; i++) {
|
|
const dot = document.getElementById('phaseDot' + i);
|
|
dot.classList.remove('active', 'completed');
|
|
if (i < phase) dot.classList.add('completed');
|
|
if (i === phase) dot.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// ============ 场景切换 ============
|
|
function showScene(sceneName) {
|
|
document.querySelectorAll('.scene').forEach(s => s.classList.remove('active'));
|
|
const scene = document.getElementById('scene' + sceneName.charAt(0).toUpperCase() + sceneName.slice(1));
|
|
if (scene) scene.classList.add('active');
|
|
state.currentScene = sceneName;
|
|
}
|
|
|
|
// ============ 模块选择 ============
|
|
function selectModule(module) {
|
|
if (module === 'filter' && !state.emotionChecked) {
|
|
addLog('warning', '需要先完成情绪模块检查');
|
|
return;
|
|
}
|
|
if (module === 'processor' && !state.logViewed) {
|
|
addLog('warning', '需要先查看滤波器日志');
|
|
return;
|
|
}
|
|
|
|
document.querySelectorAll('.module-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById('btn' + module.charAt(0).toUpperCase() + module.slice(1)).classList.add('active');
|
|
|
|
if (module === 'voice') showScene('voice');
|
|
else if (module === 'emotion') showScene('emotion');
|
|
else if (module === 'filter') showScene('filter');
|
|
else if (module === 'processor') showScene('processor');
|
|
}
|
|
|
|
// ============ 阶段1:问诊 -> 检查 ============
|
|
function startExamination() {
|
|
addLog('info', '开始检查表达模块');
|
|
updatePhase(2);
|
|
showScene('voice');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: "好,先看看发声单元的参数..." }
|
|
], () => {
|
|
startVoiceWaveform();
|
|
});
|
|
}
|
|
|
|
// ============ 阶段2:发声单元检查 ============
|
|
let voiceWaveformAnim = null;
|
|
|
|
function startVoiceWaveform() {
|
|
const canvas = document.getElementById('voiceWaveformCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = 120;
|
|
|
|
let phase = 0;
|
|
voiceWaveformAnim = setInterval(() => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.strokeStyle = '#64ffda';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
for (let x = 0; x < canvas.width; x++) {
|
|
const y = 60 +
|
|
Math.sin((x + phase) * 0.08) * 20 +
|
|
Math.sin((x + phase) * 0.15) * 10 +
|
|
(Math.random() - 0.5) * 5;
|
|
if (x === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
phase += 2;
|
|
}, 30);
|
|
}
|
|
|
|
function analyzeVoice() {
|
|
document.getElementById('btnAnalyzeVoice').disabled = true;
|
|
addLog('info', '分析语调参数...');
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('voiceAnomalyCard').style.display = 'block';
|
|
state.voiceChecked = true;
|
|
document.getElementById('btnVoice').classList.add('checked');
|
|
document.getElementById('statusVoice').textContent = '✓ 异常';
|
|
document.getElementById('voiceDataStatus').textContent = '已采集';
|
|
document.getElementById('voiceDataStatus').style.color = 'var(--accent-green)';
|
|
|
|
drawMiniWaveform('miniVoiceCanvas', '#64ffda');
|
|
addLog('warning', '发现语调异常:微颤、气息不稳');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: "奇怪...语调参数有明显异常。" },
|
|
{ speaker: "医生", text: "火山说的是笑话,但声音却在发抖。这不对劲。" },
|
|
{ speaker: "医生", text: "我需要检查一下情绪模块,看看输入端是什么情况。" }
|
|
], () => {
|
|
updatePhase(3);
|
|
selectModule('emotion');
|
|
});
|
|
}, 1500);
|
|
}
|
|
|
|
// ============ 阶段3:情绪模块检查 ============
|
|
let emotionWaveformAnim = null;
|
|
|
|
function captureEmotion() {
|
|
document.getElementById('btnCaptureEmotion').disabled = true;
|
|
addLog('info', '采集情绪波形...');
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('emotionWaveform').style.display = 'block';
|
|
startEmotionWaveform();
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('emotionCompare').style.display = 'block';
|
|
addLog('success', '波形采集完成,请与参考库对比');
|
|
}, 1000);
|
|
}, 800);
|
|
}
|
|
|
|
function startEmotionWaveform() {
|
|
const canvas = document.getElementById('emotionWaveformCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = 120;
|
|
|
|
let phase = 0;
|
|
emotionWaveformAnim = setInterval(() => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 恐惧/焦虑特征的波形:高频、不规则
|
|
ctx.strokeStyle = '#f87171';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
for (let x = 0; x < canvas.width; x++) {
|
|
const y = 60 +
|
|
Math.sin((x + phase) * 0.12) * 25 +
|
|
Math.sin((x + phase) * 0.05) * 15 +
|
|
Math.sin((x + phase) * 0.2) * 8 +
|
|
(Math.random() - 0.5) * 10;
|
|
if (x === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
phase += 3;
|
|
}, 30);
|
|
}
|
|
|
|
function selectEmotion(element, emotion, match) {
|
|
document.querySelectorAll('.emotion-option').forEach(e => e.classList.remove('selected'));
|
|
element.classList.add('selected');
|
|
state.selectedEmotion = { emotion, match };
|
|
document.getElementById('btnConfirmEmotion').disabled = false;
|
|
}
|
|
|
|
function confirmEmotion() {
|
|
if (state.selectedEmotion.emotion !== 'fear') {
|
|
showDialog([{ speaker: "系统", text: "匹配度较低,请选择更匹配的情绪类型。" }]);
|
|
return;
|
|
}
|
|
|
|
document.getElementById('emotionCompare').style.display = 'none';
|
|
document.getElementById('emotionResult').style.display = 'block';
|
|
|
|
state.emotionChecked = true;
|
|
document.getElementById('btnEmotion').classList.add('checked');
|
|
document.getElementById('statusEmotion').textContent = '✓ 恐惧';
|
|
document.getElementById('emotionDataStatus').textContent = '已采集';
|
|
document.getElementById('emotionDataStatus').style.color = 'var(--accent-orange)';
|
|
|
|
drawMiniWaveform('miniEmotionCanvas', '#f87171');
|
|
document.getElementById('btnFilter').classList.remove('locked');
|
|
document.getElementById('statusFilter').textContent = '可检查';
|
|
|
|
addLog('success', '情绪分析完成:恐惧/焦虑 (89%)');
|
|
addLog('error', '发现矛盾:输入恐惧 → 输出笑话');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: "原来如此...情绪模块输入的是恐惧和焦虑。" },
|
|
{ speaker: "医生", text: "但输出变成了笑话...输入和输出完全对不上!" },
|
|
{ speaker: "医生", text: "问题一定出在中间的信号转换过程。让我检查一下转换器。" }
|
|
], () => {
|
|
updatePhase(4);
|
|
selectModule('filter');
|
|
});
|
|
}
|
|
|
|
// ============ 阶段4:信号转换器检查 ============
|
|
function inspectFilter() {
|
|
document.getElementById('btnInspectFilter').disabled = true;
|
|
addLog('info', '检查滤波器配置...');
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('filterDetail').style.display = 'block';
|
|
state.filterChecked = true;
|
|
document.getElementById('btnFilter').classList.add('checked');
|
|
document.getElementById('statusFilter').textContent = '✓ 异常';
|
|
|
|
addLog('warning', '发现异常滤波器:情绪转换器');
|
|
addLog('error', '转换率 95%+ - 过度激活');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: "找到了!这里有一个情绪转换滤波器。" },
|
|
{ speaker: "医生", text: "它把几乎所有的负面情绪信号都转换成了幽默输出..." },
|
|
{ speaker: "医生", text: "转换率高达95%以上,这不正常。让我查查这个滤波器的来历。" }
|
|
], () => {
|
|
updatePhase(5);
|
|
showScene('log');
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
// ============ 阶段5:查看日志 ============
|
|
function viewLogHistory() {
|
|
document.getElementById('btnViewHistory').disabled = true;
|
|
addLog('info', '读取滤波器历史日志...');
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('logHistory').style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('logConclusion').style.display = 'block';
|
|
state.logViewed = true;
|
|
document.getElementById('btnProcessor').classList.remove('locked');
|
|
document.getElementById('statusProcessor').textContent = '可进入';
|
|
document.getElementById('gamePhasePanel').style.display = 'block';
|
|
|
|
addLog('success', '日志分析完成');
|
|
addLog('warning', '问题根源:过度依赖幽默防御机制');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: "原来是这样...这个滤波器是3年前安装的临时措施。" },
|
|
{ speaker: "医生", text: "但火山过度依赖了它,用笑话来掩盖所有负面情绪。" },
|
|
{ speaker: "火山", text: "我一直都是这样啊,也没出问题..." },
|
|
{ speaker: "医生", text: "火山,那些被压抑的情绪不会消失,它们会堆积。" },
|
|
{ speaker: "医生", text: "堆积久了,就会像今天这样——干扰你所有的表达。" },
|
|
{ speaker: "医生", text: "我需要进入你的处理器,帮你清理那些被压抑的表达。" },
|
|
{ speaker: "火山", text: "不...不要碰那些..." },
|
|
{ speaker: "医生", text: "火山,相信我。说出来,没那么可怕。" }
|
|
], () => {
|
|
updatePhase(6);
|
|
document.getElementById('competitionPanel').style.display = 'block';
|
|
showScene('processor');
|
|
setTimeout(() => startCleaningGame(1), 500);
|
|
});
|
|
}, 500);
|
|
}, 800);
|
|
}
|
|
|
|
// ============ 阶段6:清理游戏 ============
|
|
function startCleaningGame(round) {
|
|
state.gameRound = round;
|
|
state.isGameRunning = true;
|
|
state.allowInput = true;
|
|
|
|
const config = gameConfigs[round];
|
|
const arena = document.getElementById('particleArena');
|
|
const container = document.getElementById('particlesContainer');
|
|
const rect = arena.getBoundingClientRect();
|
|
|
|
container.innerHTML = '';
|
|
state.particles = [];
|
|
|
|
document.getElementById('languageDisplay').style.display = 'none';
|
|
document.getElementById('resultScreen').classList.remove('show');
|
|
document.getElementById('gameInstruction').textContent = '左键排斥 | 右键吸引 | 让绿色粒子连接';
|
|
document.getElementById('progressFill').style.width = '0%';
|
|
document.getElementById('progressPercent').textContent = '0%';
|
|
|
|
// 目标粒子字符
|
|
const targetChars = [
|
|
['我', '担', '心', '...'],
|
|
['我', '很', '害', '怕', '...'],
|
|
['我', '害', '怕', '被', '抛', '弃']
|
|
];
|
|
const nonTargetChars = ['哈', '笑', '话', '嘿', '呵', '好', '棒', '赞'];
|
|
const chars = targetChars[round - 1];
|
|
|
|
// 创建目标粒子
|
|
for (let i = 0; i < config.targetCount; i++) {
|
|
const particle = {
|
|
id: i,
|
|
isTarget: true,
|
|
x: Math.random() * (rect.width - 100) + 25,
|
|
y: Math.random() * (rect.height - 150) + 60,
|
|
vx: (Math.random() - 0.5) * 1.5,
|
|
vy: (Math.random() - 0.5) * 1.5,
|
|
char: chars[i] || '真'
|
|
};
|
|
state.particles.push(particle);
|
|
|
|
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.id = 'particle-' + particle.id;
|
|
container.appendChild(el);
|
|
}
|
|
|
|
// 创建干扰粒子
|
|
for (let i = 0; i < config.nonTargetCount; i++) {
|
|
const particle = {
|
|
id: config.targetCount + i,
|
|
isTarget: false,
|
|
x: Math.random() * (rect.width - 100) + 25,
|
|
y: Math.random() * (rect.height - 150) + 60,
|
|
vx: (Math.random() - 0.5) * 1.5,
|
|
vy: (Math.random() - 0.5) * 1.5,
|
|
char: nonTargetChars[i % nonTargetChars.length]
|
|
};
|
|
state.particles.push(particle);
|
|
|
|
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.id = 'particle-' + particle.id;
|
|
container.appendChild(el);
|
|
}
|
|
|
|
// 显示火山反应
|
|
showHuoshanBubble(round);
|
|
|
|
addLog('info', `开始第${round}次清理...`);
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
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'), 3500);
|
|
}
|
|
|
|
// 游戏循环
|
|
function gameLoop() {
|
|
if (!state.isGameRunning) return;
|
|
|
|
const arena = document.getElementById('particleArena');
|
|
const rect = arena.getBoundingClientRect();
|
|
|
|
state.particles.forEach(particle => {
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
|
|
// 边界反弹
|
|
if (particle.x < 10 || particle.x > rect.width - 60) particle.vx *= -0.8;
|
|
if (particle.y < 50 || particle.y > rect.height - 60) particle.vy *= -0.8;
|
|
particle.x = Math.max(10, Math.min(rect.width - 60, particle.x));
|
|
particle.y = Math.max(50, Math.min(rect.height - 60, particle.y));
|
|
|
|
// 鼠标交互
|
|
if (state.isMouseDown && state.allowInput) {
|
|
const dx = particle.x + 25 - state.mousePos.x;
|
|
const dy = particle.y + 25 - state.mousePos.y;
|
|
const dist = Math.hypot(dx, dy);
|
|
|
|
if (dist < 100 && dist > 0) {
|
|
const force = (100 - dist) / 100;
|
|
if (state.mouseButton === 0) { // 左键排斥
|
|
particle.vx += (dx / dist) * force * 2.5;
|
|
particle.vy += (dy / dist) * force * 2.5;
|
|
} 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';
|
|
}
|
|
});
|
|
|
|
drawConnections();
|
|
|
|
const progress = calculateProgress();
|
|
document.getElementById('progressFill').style.width = progress + '%';
|
|
document.getElementById('progressPercent').textContent = Math.round(progress) + '%';
|
|
|
|
// 更新拔河条
|
|
const tugPos = 85 - (progress * 0.7);
|
|
updateTugofwar(Math.max(15, tugPos));
|
|
|
|
if (progress >= 100) {
|
|
completeGameRound();
|
|
} else {
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
|
|
function calculateProgress() {
|
|
const targets = state.particles.filter(p => p.isTarget);
|
|
const nonTargets = state.particles.filter(p => !p.isTarget);
|
|
if (targets.length < 2) return 100;
|
|
|
|
const connectionDist = 90;
|
|
|
|
// BFS检查连通性
|
|
const visited = new Set([targets[0].id]);
|
|
const queue = [targets[0]];
|
|
|
|
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 connectivity = visited.size / targets.length;
|
|
|
|
// 检查是否与干扰分离
|
|
let separated = true;
|
|
for (const t of targets) {
|
|
for (const nt of nonTargets) {
|
|
if (Math.hypot(t.x - nt.x, t.y - nt.y) < connectionDist * 0.8) {
|
|
separated = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!separated) break;
|
|
}
|
|
|
|
const progress = connectivity * 70 + (separated ? 30 : 0);
|
|
return (connectivity >= 1 && separated) ? 100 : Math.min(99, progress);
|
|
}
|
|
|
|
function drawConnections() {
|
|
const svg = document.getElementById('connectionsSvg');
|
|
svg.innerHTML = '';
|
|
|
|
const targets = state.particles.filter(p => p.isTarget);
|
|
const connectionDist = 90;
|
|
|
|
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 < connectionDist) {
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', targets[i].x + 25);
|
|
line.setAttribute('y1', targets[i].y + 25);
|
|
line.setAttribute('x2', targets[j].x + 25);
|
|
line.setAttribute('y2', targets[j].y + 25);
|
|
line.setAttribute('stroke', 'rgba(74, 222, 128, 0.6)');
|
|
line.setAttribute('stroke-width', '2');
|
|
line.style.opacity = 1 - dist / connectionDist;
|
|
svg.appendChild(line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateTugofwar(position) {
|
|
state.tugofwarPosition = position;
|
|
document.getElementById('tugofwarIndicator').style.left = position + '%';
|
|
|
|
const status = document.getElementById('tugofwarStatus');
|
|
if (position < 35) {
|
|
status.textContent = '真实表达正在突破!';
|
|
status.style.color = 'var(--accent-green)';
|
|
} else if (position < 55) {
|
|
status.textContent = '双方势均力敌';
|
|
status.style.color = 'var(--accent-orange)';
|
|
} else {
|
|
status.textContent = '幽默防御主导';
|
|
status.style.color = 'var(--accent-red)';
|
|
}
|
|
}
|
|
|
|
function completeGameRound() {
|
|
state.isGameRunning = false;
|
|
const config = gameConfigs[state.gameRound];
|
|
|
|
document.getElementById('resultText').textContent = '连接成功!';
|
|
document.getElementById('resultDetail').innerHTML =
|
|
`火山说出了:<br><span style="color: var(--accent-cyan); font-size: 1.2em;">"${config.targetSentence}"</span>`;
|
|
|
|
if (state.gameRound < 3) {
|
|
document.getElementById('resultBtn').textContent = '继续';
|
|
} else {
|
|
document.getElementById('resultBtn').textContent = '查看结果';
|
|
}
|
|
|
|
addLog('success', `第${state.gameRound}次清理完成`);
|
|
addLog('info', `火山说出:"${config.targetSentence}"`);
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('resultScreen').classList.add('show');
|
|
}, 500);
|
|
}
|
|
|
|
function handleGameResult() {
|
|
document.getElementById('resultScreen').classList.remove('show');
|
|
const config = gameConfigs[state.gameRound];
|
|
|
|
if (state.gameRound < 3) {
|
|
// 回弹效果
|
|
setTimeout(() => {
|
|
updateTugofwar(config.reboundTo);
|
|
addLog('warning', '检测到回弹力...通路还不稳定');
|
|
|
|
const nextRound = state.gameRound + 1;
|
|
document.getElementById('btnGame' + nextRound).classList.remove('hidden');
|
|
|
|
showDialog([
|
|
{ speaker: "医生", text: state.gameRound === 1 ?
|
|
"连上了...但等等,它又断了?有什么在把它拉回去。" :
|
|
"有进展了!但幽默防御还在反扑..."
|
|
},
|
|
{ speaker: "医生", text: "火山,再试一次。这次说出更多。" },
|
|
{ speaker: "火山", text: state.gameRound === 1 ? "我...我不知道..." : "我...我试试..." }
|
|
], () => {
|
|
setTimeout(() => startCleaningGame(nextRound), 500);
|
|
});
|
|
}, 800);
|
|
} else {
|
|
// 最终成功
|
|
updateTugofwar(10);
|
|
addLog('success', '真实表达通路已稳定建立!');
|
|
|
|
updatePhase(7);
|
|
showFinalScene();
|
|
}
|
|
}
|
|
|
|
// ============ 阶段7:治愈结局 ============
|
|
function showFinalScene() {
|
|
showScene('final');
|
|
|
|
const dialogue = document.getElementById('finalDialogue');
|
|
dialogue.innerHTML = `
|
|
<p><span class="speaker">火山:</span>"我害怕被抛弃。"</p>
|
|
<p><span class="speaker">火山:</span>"我害怕如果不有趣,就没人要我了。"</p>
|
|
<p class="pause">(停顿)</p>
|
|
<p><span class="speaker">火山:</span>"说出来之后...好像没那么可怕。"</p>
|
|
<p><span class="speaker">医生:</span>"你做到了,火山。这次是你自己说出来的。"</p>
|
|
<p><span class="speaker">火山:</span>"...谢谢你愿意听我说真话。"</p>
|
|
<p class="pause">(火山的眼眶泛红,终于流下了积压已久的泪水)</p>
|
|
`;
|
|
|
|
updatePhase(8);
|
|
document.getElementById('patientStatus').textContent = '状态:表达通路已修复';
|
|
document.getElementById('patientStatus').style.color = 'var(--accent-green)';
|
|
}
|
|
|
|
function restartGame() {
|
|
location.reload();
|
|
}
|
|
|
|
// ============ 工具函数 ============
|
|
function drawMiniWaveform(canvasId, color) {
|
|
const canvas = document.getElementById(canvasId);
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = 40;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
|
|
for (let x = 0; x < canvas.width; x++) {
|
|
const y = 20 +
|
|
Math.sin(x * 0.15) * 8 +
|
|
Math.sin(x * 0.3) * 4 +
|
|
(Math.random() - 0.5) * 2;
|
|
if (x === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
|
|
function addLog(type, message) {
|
|
const panel = document.getElementById('logPanel');
|
|
const time = new Date().toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
});
|
|
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry ' + type;
|
|
entry.innerHTML = `<span class="log-time">[${time}]</span>${message}`;
|
|
panel.appendChild(entry);
|
|
panel.scrollTop = panel.scrollHeight;
|
|
}
|
|
|
|
// ============ 对话系统 ============
|
|
function showDialog(dialogs, callback = null) {
|
|
dialogQueue = [...dialogs];
|
|
dialogCallback = callback;
|
|
nextDialog();
|
|
}
|
|
|
|
function nextDialog() {
|
|
if (dialogQueue.length === 0) {
|
|
document.getElementById('dialogOverlay').classList.remove('show');
|
|
if (dialogCallback) {
|
|
const cb = dialogCallback;
|
|
dialogCallback = null;
|
|
cb();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const dialog = dialogQueue.shift();
|
|
const speakerEl = document.getElementById('dialogSpeaker');
|
|
speakerEl.textContent = dialog.speaker;
|
|
speakerEl.className = 'dialog-speaker' + (dialog.speaker === '火山' ? ' huoshan' : '');
|
|
document.getElementById('dialogText').textContent = dialog.text;
|
|
document.getElementById('dialogOverlay').classList.add('show');
|
|
}
|
|
|
|
document.getElementById('dialogOverlay').addEventListener('click', nextDialog);
|
|
|
|
// ============ 鼠标事件 ============
|
|
const arena = document.getElementById('particleArena');
|
|
|
|
arena.addEventListener('mousedown', (e) => {
|
|
state.isMouseDown = true;
|
|
state.mouseButton = e.button;
|
|
updateMousePos(e);
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
state.isMouseDown = false;
|
|
});
|
|
|
|
arena.addEventListener('mousemove', updateMousePos);
|
|
arena.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
function updateMousePos(e) {
|
|
const rect = arena.getBoundingClientRect();
|
|
state.mousePos.x = e.clientX - rect.left;
|
|
state.mousePos.y = e.clientY - rect.top;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|