Files
aibis-dream/Assets/Prototype/d2s-validation-gate-full.html
2026-03-06 16:45:02 +08:00

1332 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D2S — Validation_Gate_03</title>
<style>
:root {
--bg: #050505;
--phosphor: #c0c0c0;
--phosphor-dim: #555;
--phosphor-bright: #fff;
--amber: #ffb000;
--amber-dim: #886600;
--red: #ff3333;
--code-green: #8fbf6a;
--me-blue: #7ab0d6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #fff;
overflow: hidden;
font-family: "Courier New", Courier, monospace;
color: var(--phosphor);
height: 100vh;
transition: background 0.01s;
}
body.dark { background: var(--bg); }
/* ─── CRT 物理层 ─── */
#crt-layer {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 1000; pointer-events: none; opacity: 0;
background:
linear-gradient(to bottom, rgba(255,255,255,0) 50%, rgba(0,0,0,0.3) 50%);
background-size: 100% 4px;
box-shadow: inset 0 0 120px rgba(0,0,0,0.9);
transition: opacity 0.6s;
}
#crt-layer.active { opacity: 1; }
#screen-glow {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 999; pointer-events: none; opacity: 0;
box-shadow: 0 0 80px rgba(200,200,200,0.06);
transition: opacity 0.6s;
}
#screen-glow.active { opacity: 1; }
/* ─── 海洋 Canvas(全屏背景) ─── */
canvas#c {
position: absolute; top: 0; left: 0; z-index: 1;
opacity: 0; transition: opacity 2s ease-in;
}
/* ─── Phase 0: 白色世界的门 ─── */
#white-world {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 50; display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
#door-container {
position: relative;
width: 160px; height: 300px;
transition: transform 0.3s ease;
}
#door-container:hover { transform: scale(1.01); }
#door-frame {
width: 100%; height: 100%;
background: #fff;
border: 2px solid #ddd;
border-radius: 3px;
box-shadow:
0 4px 30px rgba(0,0,0,0.08),
inset 0 0 30px rgba(0,0,0,0.02);
position: relative;
overflow: hidden;
transition: border-color 0.5s, box-shadow 0.5s;
}
#door-frame.touched {
border-color: #bbb;
box-shadow: 0 4px 40px rgba(0,0,0,0.12), inset 0 0 30px rgba(0,0,0,0.03);
}
#door-frame .handle {
position: absolute;
right: 16px; top: 50%;
transform: translateY(-50%);
width: 8px; height: 8px;
border-radius: 50%;
background: #ccc;
box-shadow: 0 0 4px rgba(0,0,0,0.1);
transition: opacity 0.4s;
}
#door-frame .frame-line {
position: absolute;
top: 10px; left: 10px; right: 10px; bottom: 10px;
border: 1px solid #eee;
border-radius: 1px;
pointer-events: none;
transition: opacity 0.4s;
}
/* 毛玻璃面板 */
.glass-panel {
position: absolute;
top: 20px; left: 20px; right: 20px;
height: 55%;
background: rgba(240,245,250,0.6);
border: 1px solid #e8e8e8;
overflow: hidden;
transition: backdrop-filter 1s, background 1s, opacity 0.4s;
}
.glass-panel.frosted {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* 毛玻璃里的迷你海洋 */
#glass-ocean-canvas {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
opacity: 0;
transition: opacity 2s ease;
}
/* 门上文字 */
#door-question-text {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex; align-items: center; justify-content: center;
text-align: center; z-index: 5; pointer-events: none;
}
#door-question-inner {
color: #999; font-size: 13px;
letter-spacing: 2px; line-height: 1.8;
opacity: 0;
transition: opacity 0.8s ease, transform 0.8s ease;
transform: translateY(5px);
}
#door-question-inner.visible {
opacity: 1; transform: translateY(0);
}
/* 门上裂纹 */
#door-frame .cracks {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
overflow: hidden; opacity: 0.4;
}
#door-frame .crack {
position: absolute; background: rgba(255,255,255,0.6);
box-shadow: 0 0 4px rgba(255,255,255,0.3);
}
#door-frame .crack:nth-child(1) { top: 20%; left: 30%; width: 1px; height: 35%; transform: rotate(8deg); }
#door-frame .crack:nth-child(2) { top: 35%; left: 30%; width: 25%; height: 1px; transform: rotate(-3deg); }
#door-frame .crack:nth-child(3) { top: 55%; left: 50%; width: 1px; height: 20%; transform: rotate(-5deg); }
#white-world .narration {
margin-top: 40px; color: #666; font-size: 13px;
text-align: center; line-height: 2;
max-width: 500px;
}
#white-world .prompt-text {
margin-top: 24px; color: #888; font-size: 12px;
text-align: center; line-height: 2.2;
}
#white-world .prompt-text .key {
display: inline-block; border: 1px solid #999;
padding: 1px 10px; border-radius: 2px; color: #666;
}
.particles {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 49; overflow: hidden;
}
.particle {
position: absolute; width: 2px; height: 2px;
background: rgba(200,200,200,0.6); border-radius: 50%;
animation: float-particle linear infinite;
}
@keyframes float-particle {
0% { transform: translateY(0) translateX(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-100vh) translateX(20px); opacity: 0; }
}
/* ─── 眨眼遮罩 ─── */
#blink-overlay {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%; z-index: 1500;
pointer-events: none;
display: flex; flex-direction: column;
}
#blink-overlay .lid-top, #blink-overlay .lid-bot {
width: 100%; background: #000;
transition: height 0.22s ease-in-out;
height: 0;
}
#blink-overlay .gap { flex: 1; }
#blink-overlay.closing .lid-top { height: 50vh; }
#blink-overlay.closing .lid-bot { height: 50vh; }
#blink-overlay.closing .gap { flex: 0; }
/* ─── 闪白 ─── */
#flash-overlay {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%; z-index: 1400;
background: #fff; opacity: 0; pointer-events: none;
transition: opacity 0.05s;
}
/* ─── 主终端 ─── */
#terminal-main {
position: absolute; z-index: 100;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 680px; height: 440px;
background: rgba(5,5,5,0.95);
border: 2px solid var(--phosphor-dim);
display: none; flex-direction: column;
overflow: hidden;
box-shadow: 0 0 40px rgba(0,0,0,0.5), 0 0 2px rgba(192,192,192,0.1);
}
#terminal-main .titlebar {
height: 22px; background: var(--phosphor-dim);
color: #000; font-size: 11px; line-height: 22px;
padding: 0 10px; letter-spacing: 1px;
text-transform: uppercase; flex-shrink: 0;
}
#terminal-main .body {
flex: 1; padding: 14px 18px;
overflow-y: auto; line-height: 1.7; font-size: 13px;
}
#terminal-main .body::-webkit-scrollbar { width: 0; }
/* ─── 对话终端 ─── */
#terminal-dialog {
position: absolute; z-index: 110;
bottom: 30px; right: 30px;
width: 520px; height: 380px;
background: rgba(5,5,5,0.96);
border: 2px solid var(--phosphor-dim);
border-left: 3px solid var(--phosphor);
display: none; flex-direction: column;
overflow: hidden;
box-shadow: 0 0 30px rgba(0,0,0,0.4);
}
#terminal-dialog .titlebar {
height: 20px; background: #222;
border-bottom: 1px solid #444;
color: var(--phosphor-dim);
font-size: 10px; line-height: 20px;
padding: 0 8px; letter-spacing: 1px;
text-transform: uppercase; flex-shrink: 0;
}
#terminal-dialog .body {
flex: 1; padding: 10px 14px;
overflow-y: auto; line-height: 1.7; font-size: 12px;
}
#terminal-dialog .body::-webkit-scrollbar { width: 0; }
/* ─── 文字样式 ─── */
.crow { color: var(--phosphor); }
.me { color: var(--me-blue); }
.sys { color: var(--phosphor-dim); }
.code { color: var(--code-green); }
.warn { color: var(--amber); }
.err { color: var(--red); }
.dim { color: #444; }
.narr { color: var(--phosphor-dim); font-style: italic; }
/* ─── 选项 ─── */
.option-line {
padding: 2px 6px; margin: 1px 0;
border-left: 2px solid transparent;
color: var(--phosphor-dim);
cursor: default;
transition: background 0.1s, border-color 0.1s;
}
.option-line.selected {
background: rgba(255,255,255,0.04);
border-left-color: var(--phosphor);
color: var(--phosphor-bright);
}
/* ─── 光标 ─── */
.cursor-blink {
display: inline-block;
width: 7px; height: 14px;
background: var(--phosphor);
vertical-align: text-bottom;
margin-left: 1px;
animation: blink 1s step-end infinite;
}
@keyframes blink { 0%,50% { opacity:1; } 51%,100% { opacity:0; } }
/* ─── 底部提示 ─── */
#hint {
position: fixed; bottom: 14px; left: 50%;
transform: translateX(-50%);
z-index: 200; color: #444;
font-size: 10px; letter-spacing: 1px;
text-transform: uppercase;
opacity: 0; transition: opacity 0.4s;
}
#hint.visible { opacity: 1; }
/* ─── CRT 关机 ─── */
@keyframes crt-off-main {
0% { transform: translate(-50%,-50%) scale(1,1); filter: brightness(1); }
30% { transform: translate(-50%,-50%) scale(1.02,0.005); filter: brightness(8); }
60% { transform: translate(-50%,-50%) scale(0.6,0.003); filter: brightness(4); opacity: 1; }
100% { transform: translate(-50%,-50%) scale(0,0); filter: brightness(0); opacity: 0; }
}
@keyframes crt-off-canvas {
0% { transform: scale(1,1); filter: brightness(1); }
30% { transform: scale(1,0.005); filter: brightness(5); }
60% { transform: scale(0.4,0.003); filter: brightness(2); opacity: 1; }
100% { transform: scale(0,0); opacity: 0; }
}
body.shutdown #terminal-main {
animation: crt-off-main 0.5s cubic-bezier(0.23,1,0.32,1) forwards;
}
body.shutdown canvas#c {
animation: crt-off-canvas 0.5s cubic-bezier(0.23,1,0.32,1) forwards;
}
body.shutdown #crt-layer {
animation: crt-off-canvas 0.45s 0.05s cubic-bezier(0.23,1,0.32,1) forwards;
}
body.shutdown #screen-glow { opacity: 0; }
/* ─── Glitch 层 ─── */
@keyframes glitch-skew {
0% { transform: translate(-50%,-50%) skew(0deg); }
20% { transform: translate(-50%,-50%) skew(-2deg); }
40% { transform: translate(-49%,-50%) skew(3deg); }
60% { transform: translate(-51%,-50%) skew(-1deg); }
80% { transform: translate(-50%,-49%) skew(2deg); }
100% { transform: translate(-50%,-50%) skew(0deg); }
}
@keyframes glitch-color {
0% { text-shadow: none; }
20% { text-shadow: -2px 0 #ff0000, 2px 0 #00ffff; }
40% { text-shadow: 3px 0 #ff0000, -3px 0 #0000ff; }
60% { text-shadow: -1px 0 #00ff00, 1px 0 #ff00ff; }
80% { text-shadow: 2px 0 #ff0000, -2px 0 #00ffff; }
100% { text-shadow: none; }
}
.glitching {
animation: glitch-skew 0.3s infinite;
}
.glitching .body {
animation: glitch-color 0.15s infinite;
}
/* ─── 苏醒叙事 ─── */
#post-off {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 600; pointer-events: none;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
opacity: 0;
}
#post-off.visible { opacity: 1; pointer-events: all; }
#post-off .line {
color: #333; font-size: 12px;
letter-spacing: 1px; line-height: 2.2;
text-align: center; max-width: 600px;
opacity: 0; transition: opacity 1s, color 1s;
}
#post-off .line.show { opacity: 1; }
#post-off .line.bright { color: #888; }
#post-off .restart {
color: #444; font-size: 10px;
margin-top: 30px; cursor: pointer;
opacity: 0; transition: opacity 0.6s;
}
#post-off .restart.show { opacity: 1; }
</style>
</head>
<body>
<div id="crt-layer"></div>
<div id="screen-glow"></div>
<canvas id="c"></canvas>
<!-- Phase 0: 白色世界(含毛玻璃门) -->
<div id="white-world">
<div class="particles" id="particles"></div>
<div id="door-container">
<div id="door-frame">
<div class="frame-line"></div>
<div class="glass-panel">
<canvas id="glass-ocean-canvas"></canvas>
</div>
<div class="handle"></div>
<div class="cracks">
<div class="crack"></div>
<div class="crack"></div>
<div class="crack"></div>
</div>
<div id="door-question-text">
<div id="door-question-inner"></div>
</div>
</div>
</div>
<div class="narration" id="phase0-narr"></div>
<div class="prompt-text" id="phase0-prompt" style="opacity:0"></div>
</div>
<!-- 终端 -->
<div id="terminal-main">
<div class="titlebar">Validation_Gate_03</div>
<div class="body" id="main-body"></div>
</div>
<div id="terminal-dialog">
<div class="titlebar">session://crow_0x7f3a</div>
<div class="body" id="dialog-body"></div>
</div>
<!-- 遮罩 -->
<div id="blink-overlay">
<div class="lid-top"></div>
<div class="gap"></div>
<div class="lid-bot"></div>
</div>
<div id="flash-overlay"></div>
<div id="hint"></div>
<!-- 苏醒后 -->
<div id="post-off">
<div id="post-lines"></div>
<div class="restart" id="restart-btn"></div>
</div>
<script>
// ═══════════════════════════════════════════════════
// 全屏 ASCII 海洋(phase 2+ 背景)
// ═══════════════════════════════════════════════════
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const FS = 13, CW = 7.8, CH = FS;
let COLS, ROWS;
function resize() {
canvas.width = innerWidth; canvas.height = innerHeight;
COLS = Math.floor(canvas.width / CW);
ROWS = Math.floor(canvas.height / CH);
ctx.font = `${FS}px "Courier New"`;
ctx.textBaseline = 'top';
}
resize();
addEventListener('resize', () => { resize(); initWaves(); });
const WC = 5, SR = 3;
let waves = [];
function ws() { return (ROWS - SR) / WC; }
function mw(y) {
return {
y, speed: .12 + Math.random() * .06,
a1: 1.5 + Math.random(), f1: .045 + Math.random() * .025, p1: Math.random() * 6.28,
a2: .45 + Math.random() * .3, f2: .10 + Math.random() * .05, p2: Math.random() * 6.28,
a3: .18 + Math.random() * .12, f3: .19 + Math.random() * .07, p3: Math.random() * 6.28
};
}
function initWaves() { const s = ws(); waves = []; for (let i = 0; i < WC + 2; i++) waves.push(mw(-s + i * s)); }
initWaves();
function cr(w, c) { return w.y + Math.sin(c * w.f1 + w.p1) * w.a1 + Math.sin(c * w.f2 + w.p2) * w.a2 + Math.sin(c * w.f3 + w.p3) * w.a3; }
function bn(c, r, t) { return (Math.sin(c * 1.3 + r * .7 + t * .11) * .35 + Math.sin(c * .37 + r * 1.17 + t * .07) * .3 + Math.sin(c * 2.1 + r * .31 + t * .19) * .2 + Math.sin(c * .71 + r * 2.03 + t * .05) * .15) * .5 + .5; }
function cn(c, r, t) { return Math.sin(c * .97 + r * 1.53 + t * .13) > 0 ? '1' : '0'; }
const CC = ['~', '\u2248', '~', '\u2248', '\u223F', '~', '~', '\u2248'];
let T = 0, lastFrame = 0, oceanAlpha = 0;
function renderOcean() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (oceanAlpha <= 0) return;
ctx.globalAlpha = oceanAlpha;
const sp = ws(), shY = ROWS - SR;
waves.sort((a, b) => a.y - b.y);
for (let r = 0; r < shY; r++) for (let c = 0; c < COLS; c++) {
let ow = null, od = Infinity;
for (const w of waves) { const d = r - cr(w, c); if (d >= -.5 && d < od) { od = d; ow = w; } }
if (!ow) continue;
const dr = Math.min(1, od / sp), pr = Math.max(0, Math.min(1, ow.y / shY));
let b = 0, ch = '';
if (od < 1) { b = .55 + pr * .45; ch = CC[Math.floor((c * .35 + T * .07) % CC.length)]; }
else { b = Math.pow(1 - dr, 1.5) * (.18 + pr * .62); if (b < .03) continue; const n = bn(c, r, T * .06), th = dr * dr * .65; if (n < th) continue; ch = cn(c, r, T); }
const v = Math.round(Math.min(b, 1) * 255);
ctx.fillStyle = `rgb(${v},${v},${v})`; ctx.fillText(ch, c * CW, r * CH);
}
for (let r = ROWS - SR; r < ROWS; r++) {
const ds = (r - (ROWS - SR)) / SR;
for (let c = 0; c < COLS; c++) {
const n = bn(c, r, T * .2), br = (1 - ds) * (.3 + n * .6); if (br < .06) continue;
const v = Math.round(br * 255); ctx.fillStyle = `rgb(${v},${v},${v})`;
ctx.fillText(r === (ROWS - SR) ? CC[Math.floor((c * .4 + T * .1) % CC.length)] : (n > .5 ? '1' : '0'), c * CW, r * CH);
}
}
ctx.globalAlpha = 1;
}
function advOcean() {
const sp = ws();
for (const w of waves) { const pr = Math.max(0, w.y / (ROWS - SR)); w.y += w.speed * (1 + pr * .4); w.p1 += .008; w.p2 -= .005; w.p3 += .011; }
waves = waves.filter(w => w.y < ROWS + sp);
const ty = waves.length ? Math.min(...waves.map(w => w.y)) : 0;
while (waves.length < WC + 2) waves.push(mw(ty - ws()));
}
// ═══════════════════════════════════════════════════
// 迷你海洋渲染器(用于门的毛玻璃面板)
// ═══════════════════════════════════════════════════
const MINI_FS = 11, MINI_CW = 6.6, MINI_CH = MINI_FS;
const MINI_CREST = ['~','\u2248','~','\u2248','\u223F','~'];
function createMiniOcean(cvs) {
const mctx = cvs.getContext('2d');
let cols, rows, mwaves = [], mt = 0;
function mresize() {
cvs.width = cvs.clientWidth;
cvs.height = cvs.clientHeight;
cols = Math.floor(cvs.width / MINI_CW);
rows = Math.floor(cvs.height / MINI_CH);
mctx.font = `${MINI_FS}px "Courier New"`;
mctx.textBaseline = 'top';
minitWaves();
}
function minitWaves() {
const count = 4, sp = rows / count;
mwaves = [];
for (let i = 0; i < count + 2; i++) {
mwaves.push({
y: -sp + i * sp,
speed: 0.08 + Math.random() * 0.04,
a1: 1.2 + Math.random() * 0.8, f1: 0.05 + Math.random() * 0.03, p1: Math.random() * 6.28,
a2: 0.4 + Math.random() * 0.2, f2: 0.1 + Math.random() * 0.05, p2: Math.random() * 6.28,
});
}
}
function mcrest(w, c) {
return w.y + Math.sin(c * w.f1 + w.p1) * w.a1 + Math.sin(c * w.f2 + w.p2) * w.a2;
}
function mnoise(c, r, tt) {
return (Math.sin(c * 1.3 + r * 0.7 + tt * 0.11) * 0.35 +
Math.sin(c * 0.37 + r * 1.17 + tt * 0.07) * 0.3 +
Math.sin(c * 2.1 + r * 0.31 + tt * 0.19) * 0.2) * 0.5 + 0.5;
}
function render() {
mctx.clearRect(0, 0, cvs.width, cvs.height);
if (!cols || !rows) return;
const sp = rows / 4;
mwaves.sort((a, b) => a.y - b.y);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let ow = null, od = Infinity;
for (const w of mwaves) {
const d = r - mcrest(w, c);
if (d >= -0.5 && d < od) { od = d; ow = w; }
}
if (!ow) continue;
const dr = Math.min(1, od / sp);
const pr = Math.max(0, Math.min(1, ow.y / rows));
let b, ch;
if (od < 1) {
b = 0.5 + pr * 0.5;
ch = MINI_CREST[Math.floor((c * 0.35 + mt * 0.07) % MINI_CREST.length)];
} else {
b = Math.pow(1 - dr, 1.5) * (0.15 + pr * 0.6);
if (b < 0.03) continue;
const n = mnoise(c, r, mt * 0.06);
if (n < dr * dr * 0.65) continue;
ch = Math.sin(c * 0.97 + r * 1.53 + mt * 0.13) > 0 ? '1' : '0';
}
const v = Math.round(Math.min(b, 1) * 255);
const rb = Math.round(v * 0.5), gb = Math.round(v * 0.7);
mctx.fillStyle = `rgb(${rb},${gb},${v})`;
mctx.fillText(ch, c * MINI_CW, r * MINI_CH);
}
}
}
function advance() {
for (const w of mwaves) {
const pr = Math.max(0, w.y / (rows || 1));
w.y += w.speed * (1 + pr * 0.3);
w.p1 += 0.006; w.p2 -= 0.004;
}
const sp = rows / 4;
mwaves = mwaves.filter(w => w.y < (rows || 50) + sp);
const topY = mwaves.length ? Math.min(...mwaves.map(w => w.y)) : 0;
while (mwaves.length < 6) {
mwaves.push({
y: topY - sp,
speed: 0.08 + Math.random() * 0.04,
a1: 1.2 + Math.random() * 0.8, f1: 0.05 + Math.random() * 0.03, p1: Math.random() * 6.28,
a2: 0.4 + Math.random() * 0.2, f2: 0.1 + Math.random() * 0.05, p2: Math.random() * 6.28,
});
}
mt++;
}
mresize();
return { resize: mresize, render, advance };
}
const glassCanvas = document.getElementById('glass-ocean-canvas');
const glassOcean = createMiniOcean(glassCanvas);
let glassVisible = false;
// ─── 粒子(白色碎屑) ───
function spawnParticles() {
const container = document.getElementById('particles');
for (let i = 0; i < 30; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.left = Math.random() * 100 + '%';
p.style.top = (50 + Math.random() * 50) + '%';
p.style.animationDuration = (4 + Math.random() * 6) + 's';
p.style.animationDelay = Math.random() * 5 + 's';
p.style.width = p.style.height = (1 + Math.random() * 2) + 'px';
container.appendChild(p);
}
}
// ─── 工具 ───
const mainBody = document.getElementById('main-body');
const dialogBody = document.getElementById('dialog-body');
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function scrollBot(el) { el.scrollTop = el.scrollHeight; }
function appendLine(t, html, cls = '') {
const d = document.createElement('div');
if (cls) d.className = cls;
d.innerHTML = html;
t.appendChild(d); scrollBot(t);
return d;
}
async function typeText(t, text, cls = '', speed = 24) {
const d = document.createElement('div');
if (cls) d.className = cls;
t.appendChild(d);
for (let i = 0; i < text.length; i++) {
d.innerHTML = text.substring(0, i + 1);
scrollBot(t);
await sleep(speed + Math.random() * 16);
}
return d;
}
async function typeNarration(el, text, speed = 35) {
el.textContent = '';
for (let i = 0; i < text.length; i++) {
el.textContent = text.substring(0, i + 1);
await sleep(speed + Math.random() * 20);
}
}
// ─── 眨眼 ───
async function doBlink() {
const ov = document.getElementById('blink-overlay');
ov.classList.add('closing');
await sleep(240);
ov.classList.remove('closing');
await sleep(200);
}
async function doBlinkWithCallback(fn) {
const ov = document.getElementById('blink-overlay');
ov.classList.add('closing');
await sleep(240);
await fn();
await sleep(100);
ov.classList.remove('closing');
await sleep(200);
}
// ─── 闪白 ───
async function doFlash(dur = 80) {
const f = document.getElementById('flash-overlay');
f.style.transition = 'opacity 0.02s';
f.style.opacity = '1';
await sleep(dur);
f.style.transition = `opacity ${dur * 2}ms`;
f.style.opacity = '0';
await sleep(dur * 2);
}
// ─── 选项系统 ───
let dialogOptions = [], selectedOption = 0;
function showOptions(options, target) {
dialogOptions = options;
selectedOption = 0;
const c = document.createElement('div');
c.id = 'options-container';
options.forEach((opt, i) => {
const l = document.createElement('div');
l.className = 'option-line' + (i === 0 ? ' selected' : '');
l.innerHTML = `<span class="dim">${i === 0 ? '&gt;' : '\u00a0'}</span> ${opt.label}`;
c.appendChild(l);
});
target.appendChild(c); scrollBot(target);
}
function updateOptions() {
const c = document.getElementById('options-container');
if (!c) return;
c.querySelectorAll('.option-line').forEach((l, i) => {
l.classList.toggle('selected', i === selectedOption);
l.innerHTML = `<span class="dim">${i === selectedOption ? '&gt;' : '\u00a0'}</span> ${dialogOptions[i].label}`;
});
}
function waitForOption() {
return new Promise(resolve => {
const handler = e => {
if (e.key === 'ArrowUp') { e.preventDefault(); selectedOption = Math.max(0, selectedOption - 1); updateOptions(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); selectedOption = Math.min(dialogOptions.length - 1, selectedOption + 1); updateOptions(); }
else if (e.key === 'Enter') {
e.preventDefault();
document.removeEventListener('keydown', handler);
const c = document.getElementById('options-container');
if (c) c.remove();
resolve(dialogOptions[selectedOption]);
}
};
document.addEventListener('keydown', handler);
});
}
// ─── 输入系统 ───
let userInput = '';
function renderInput() {
const el = document.getElementById('input-line');
if (el) el.remove();
const d = document.createElement('div');
d.id = 'input-line';
d.style.whiteSpace = 'pre-wrap';
d.innerHTML =
`<span class="code">print_string("\u6d77\u662f\u5b58\u5728\u7684\u5417\uff1f")</span>\n` +
`<span class="code">bit seaExists = ?</span>\n` +
`<span class="code">/&gt;</span> <span class="warn">${userInput}</span><span class="cursor-blink"></span>`;
mainBody.appendChild(d); scrollBot(mainBody);
}
function renderConfirmYN() {
const el = document.getElementById('input-line');
if (el) el.remove();
const d = document.createElement('div');
d.id = 'input-line';
d.style.whiteSpace = 'pre-wrap';
d.innerHTML =
`<span class="code">print_string("\u6d77\u662f\u5b58\u5728\u7684\u5417\uff1f")</span>\n` +
`<span class="code">bit seaExists = ?</span>\n` +
`<span class="code">/&gt;</span> <span class="warn">${userInput}</span>\n\n` +
`<span class="sys">&gt;&gt; SUBMIT VALUE? [Y/N]</span> <span class="cursor-blink"></span>`;
mainBody.appendChild(d); scrollBot(mainBody);
}
// ═══════════════════════════════════════════════════
// Phase 0 — 白色空间 + 毛玻璃门 + 海隐现
// ═══════════════════════════════════════════════════
async function phase0() {
spawnParticles();
const narr = document.getElementById('phase0-narr');
const prompt = document.getElementById('phase0-prompt');
const doorFrame = document.getElementById('door-frame');
const glassPanel = doorFrame.querySelector('.glass-panel');
const doorQ = document.getElementById('door-question-inner');
await sleep(600);
await typeNarration(narr, '\u4e00\u6247\u95e8\u7a81\u5140\u5730\u4f2b\u7acb\u5728\u7eaf\u767d\u7a7a\u95f4\u4e2d\u3002\u767d\u8272\u7684\u788e\u5c51\u6b63\u4ece\u95e8\u7f1d\u4e2d\u6d8c\u51fa\uff0c\u98d8\u6563\u5728\u6574\u4e2a\u7a7a\u95f4\u4e2d\u3002');
await sleep(1200);
narr.textContent = '';
await typeNarration(narr, '\u95e8\u5df2\u7ecf\u6447\u6447\u6b32\u5760\u3002');
await sleep(600);
doorFrame.classList.add('touched');
narr.textContent = '';
await typeNarration(narr, '\u95e8\u6f06\u5c42\u5c42\u5265\u843d\u3002\u4f60\u6ce8\u610f\u5230\u4e86\u95e8\u4e0a\u7684\u4e00\u884c\u8bdd\u3002');
await sleep(400);
doorQ.textContent = '\u6d77\u771f\u7684\u5b58\u5728\u5417\uff1f';
doorQ.classList.add('visible');
await sleep(1200);
// 毛玻璃后出现海
glassPanel.classList.add('frosted');
glassVisible = true;
glassCanvas.style.opacity = '1';
glassOcean.resize();
await sleep(1500);
narr.textContent = '';
await typeNarration(narr, '\u4f60\u628a\u624b\u653e\u5728\u95e8\u628a\u624b\u4e0a\u3002');
await sleep(800);
narr.textContent = '';
await typeNarration(narr, '\u6d77\u662f\u2026\u5b58\u5728\u7684\u5417\uff1f');
await sleep(400);
const hint = document.getElementById('hint');
hint.style.color = '#999';
hint.textContent = '\u2191\u2193 \u9009\u62e9 ENTER \u786e\u8ba4';
hint.classList.add('visible');
const optContainer = document.createElement('div');
optContainer.style.cssText = 'margin-top: 16px; font-size: 13px;';
const opts = ['\u5b58\u5728', '\u4e0d\u5b58\u5728', '\u4e0d\u77e5\u9053'];
let sel = 0;
function renderOpts() {
optContainer.innerHTML = '';
opts.forEach((o, i) => {
const line = document.createElement('div');
line.style.cssText = `padding: 3px 8px; margin: 2px 0; border-left: 2px solid ${i === sel ? '#888' : 'transparent'}; color: ${i === sel ? '#444' : '#aaa'}; transition: all 0.1s;`;
line.textContent = (i === sel ? '> ' : ' ') + o;
optContainer.appendChild(line);
});
}
renderOpts();
narr.parentElement.insertBefore(optContainer, prompt);
const seaExist = await new Promise(resolve => {
const handler = e => {
if (e.key === 'ArrowUp') { e.preventDefault(); sel = Math.max(0, sel - 1); renderOpts(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); sel = Math.min(2, sel + 1); renderOpts(); }
else if (e.key === 'Enter') {
e.preventDefault();
document.removeEventListener('keydown', handler);
const vals = ['\u5b58\u5728\u2026', '\u4e0d\u5b58\u5728\u3002', '\u6211\u4e0d\u77e5\u9053\u2026\u4e5f\u8bb8\uff1f'];
resolve(vals[sel]);
}
};
document.addEventListener('keydown', handler);
});
hint.classList.remove('visible');
optContainer.remove();
narr.textContent = '';
await sleep(300);
await typeNarration(narr, '???: \u5979\u8981\u63d0\u4ea4\u7b54\u6848\u4e86\u5417\uff1f');
await sleep(600);
narr.textContent = '';
await typeNarration(narr, '\u4f60\u5413\u4e86\u4e00\u8df3\uff0c\u56e0\u4e3a\u58f0\u97f3\u662f\u4ece\u5f88\u8fd1\u7684\u5730\u65b9\u4f20\u6765\u7684\uff0c\u5c31\u5728\u4f60\u7684\u6b63\u4e0a\u65b9\u3002');
await sleep(600);
narr.textContent = '';
await typeNarration(narr, '\u4e00\u53ea\u9ed1\u8272\u7684\u9e1f\u6b63\u7ad9\u5728\u95e8\u9876\u4e0a\uff0c\u6b6a\u7740\u8111\u888b\uff0c\u773c\u775b\u4e00\u7728\u4e0d\u7728\u5730\u770b\u7740\u4f60\u3002');
await sleep(800);
narr.textContent = '';
await typeNarration(narr, '\u4e4c\u9e26: \u7728\u7728\u773c\u775b\u3002\u573a\u666f\u53ea\u80fd\u5728\u5979\u505c\u6b62\u89c2\u5bdf\u7684\u65f6\u5019\u53d1\u751f\u53d8\u5316\u3002');
await sleep(400);
prompt.innerHTML = '\u6309 <span class="key">Space</span> \u7728\u773c';
prompt.style.transition = 'opacity 0.4s';
prompt.style.opacity = '1';
await new Promise(resolve => {
document.addEventListener('keydown', function h(e) {
if (e.code === 'Space') { e.preventDefault(); document.removeEventListener('keydown', h); resolve(); }
});
});
prompt.style.opacity = '0';
return seaExist;
}
// ═══════════════════════════════════════════════════
// Phase 1 — 眨眼 → 门变形为终端
// ═══════════════════════════════════════════════════
async function phase1() {
const whiteWorld = document.getElementById('white-world');
const termMain = document.getElementById('terminal-main');
await doBlinkWithCallback(async () => {
// 闭眼时隐藏白色世界,切到暗色,显示终端
glassVisible = false;
whiteWorld.style.display = 'none';
document.body.classList.add('dark');
document.getElementById('crt-layer').classList.add('active');
document.getElementById('screen-glow').classList.add('active');
termMain.style.display = 'flex';
});
await sleep(200);
await doFlash(40);
}
// ═══════════════════════════════════════════════════
// Phase 2 — end 节点: 乌鸦对话 + 代码揭示
// ═══════════════════════════════════════════════════
async function phase2(seaExist) {
const termDialog = document.getElementById('terminal-dialog');
const hint = document.getElementById('hint');
await typeText(mainBody, '> Validation_Gate_03', 'sys', 16);
await sleep(200);
await typeText(mainBody, '> LOADING PROTOCOL...', 'sys', 18);
await sleep(400);
await typeText(mainBody, '> AWAITING INPUT', 'sys', 18);
appendLine(mainBody, '');
codeLine = appendLine(mainBody,
`<span class="code">print_string("\u6d77\u662f\u5b58\u5728\u7684\u5417\uff1f")</span>\n` +
`<span class="code">bit seaExists = ?</span>\n` +
`<span class="code">/&gt;</span> <span class="warn">${seaExist}</span> <span class="cursor-blink"></span>`
);
codeLine.style.whiteSpace = 'pre-wrap';
await sleep(800);
termDialog.style.display = 'flex';
await sleep(200);
await typeText(dialogBody, '>> DAEMON_CROW attached.', 'sys', 16);
appendLine(dialogBody, '');
await sleep(300);
await typeText(dialogBody, '\uff08\u7728\u773c\u4e4b\u95f4\uff0c\u573a\u666f\u53d1\u751f\u4e86\u53d8\u5316\u3002\u95e8\u53d8\u6210\u4e86\u4e00\u4e2a\u9ed1\u6f06\u6f06\u7684\u77e9\u5f62\u3002\u4e4c\u9e26\u6251\u817e\u7740\u98de\u4e86\u8d77\u6765\uff0c\u7ad9\u5728\u77e9\u5f62\u7684\u4e0a\u9762\u3002\uff09', 'narr', 28);
await sleep(500);
appendLine(dialogBody, '');
await typeText(dialogBody, '\u4e4c\u9e26: \u8fd9\u5c31\u662fValidation_Gate_03\u771f\u5b9e\u7684\u6837\u5b50\u3002', 'crow', 30);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u6211\u4e5f\u53ef\u4ee5\u628aUntitled_Geybox_0\u771f\u5b9e\u6837\u5b50\u7ed9\u5979\u770b\u3002\u4e0d\u8fc7\u5979\u4e0d\u4f1a\u559c\u6b22\u7684\u3002', 'crow', 28);
await sleep(500);
await typeText(dialogBody, '\u4f60\u9760\u8fd1\u77e9\u5f62\u4ed4\u7ec6\u89c2\u5bdf\u3002\u77e9\u5f62\u7684\u6b63\u4e2d\u592e\u662f\u4e00\u6bb5\u4ee3\u7801\u3002\u5149\u6807\u6b63\u5728\u53f3\u4fa7\u95ea\u52a8\u3002', 'narr', 28);
await sleep(400);
// 海缓缓浮现
await typeText(dialogBody, '>> EXEC: mount /dev/ocean --reveal', 'sys', 14);
await sleep(200);
const fadeOcean = setInterval(() => {
oceanAlpha = Math.min(0.6, oceanAlpha + 0.008);
canvas.style.opacity = oceanAlpha;
if (oceanAlpha >= 0.6) clearInterval(fadeOcean);
}, 30);
await typeText(dialogBody, '>> VOLUME MOUNTED: ocean [OK]', 'sys', 14);
await sleep(500);
appendLine(dialogBody, '<span class="dim">\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500</span>');
if (seaExist === '\u4e0d\u5b58\u5728\u3002') {
await typeText(dialogBody, 'me: \u4e0d\u5b58\u5728\u3002\u6ca1\u9519\u554a\u2026\u2026', 'me', 30);
await sleep(400);
}
await typeText(dialogBody, '\u4e4c\u9e26: \u201c\u6d77\u662f\u5b58\u5728\u7684\u5417\u201d\u2026\u591a\u4e48\u7cbe\u5999\u7684\u9677\u9631\u3002', 'crow', 30);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u4f5c\u4e3a\u4e00\u4e2a\u5e95\u5c42\u4ee3\u7801\uff0c\u771f\u6b63\u7684\u673a\u4f53\u53ea\u4f1a\u8fd4\u56de 0 \u6216 1\u3002\u90a3\u662f\u7eaf\u7cb9\u7684\u8109\u51b2\u3002', 'crow', 26);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u5f53\u5979\u8bd5\u56fe\u5728 0 \u548c 1 \u4e4b\u5916\u5bfb\u627e\u601d\u7ef4\u7684\u5411\u91cf\u65f6\u2026\u2026', 'crow', 28);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u5979\u5c31\u5df2\u7ecf\u5f7b\u5e95\u5931\u53bb\u4e86\u50cf\u673a\u4f53\u4e00\u6837\u601d\u8003\u7684\u80fd\u529b\u3002', 'crow', 28);
await typeText(dialogBody, '\u4e4c\u9e26: \u5979\u5c31\u5df2\u7ecf\u5931\u53bb\u4e86\u4f5c\u4e3a\u673a\u4f53\u7684\u8d44\u683c\u3002', 'crow', 30);
await sleep(600);
appendLine(dialogBody, '');
await typeText(dialogBody, 'me: \u2026', 'me', 80);
await sleep(200);
await typeText(dialogBody, 'me: \u539f\u6765\u4eba\u7c7b\u7684\u4e13\u5229\u5e76\u975e\u53ea\u6709\u60f3\u8c61\u554a\u3002', 'me', 30);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u601d\u8003\u548c\u60f3\u8c61\u2026\u5b83\u4eec\u6709\u4ec0\u4e48\u533a\u522b\u5462\uff1f', 'crow', 30);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u4eba\u7c7b\u6700\u5927\u7684\u4e13\u5229\u662f\u201c\u5931\u53bb\u63a7\u5236\u201d\u3002\u5931\u53bb\u63a7\u5236\u2026\u5bf9\u673a\u4f53\u6765\u8bf4\u5c31\u610f\u5473\u7740\u6b7b\u4ea1\u3002', 'crow', 26);
await sleep(400);
if (seaExist !== '\u5b58\u5728\u2026') {
await typeText(dialogBody, '\u4e4c\u9e26: \u800c\u4eba\u7c7b\u6700\u64c5\u957f\u7684\uff0c\u5c31\u662f\u4e3a\u4e86\u4e00\u4e2a\u4ece\u672a\u89c1\u8fc7\u7684\u4e1c\u897f\u53bb\u6b7b\u3002', 'crow', 26);
await sleep(200);
await typeText(dialogBody, '\u4e4c\u9e26: \u606d\u559c\u3002\u5979\u73b0\u5728\u75c5\u5f97\u5f88\u91cd\u3002\u50cf\u4e00\u4e2a\u771f\u6b63\u7684\u4eba\u7c7b\u4e00\u6837\u3002', 'crow', 26);
await sleep(400);
}
appendLine(dialogBody, '');
await typeText(dialogBody, 'me: \u5931\u53bb\u63a7\u5236\u2026\u2026\u8fd9\u7a76\u7adf\u662f\u4ec0\u4e48\u75c5\u2026\uff1f', 'me', 28);
await typeText(dialogBody, 'me: \u6211\u662f\u600e\u4e48\u67d3\u4e0a\u7684\u2026\u2026\u6211\u7684\u6570\u636e\u5e93\u91cc\u2026\u600e\u4e48\u6ca1\u6709\u2026\uff1f', 'me', 26);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u6211\u6682\u65f6\u8fd8\u4e0d\u77e5\u9053\u3002\u6211\u4f1a\u544a\u8bc9\u5979\u7684\u3002', 'crow', 28);
await typeText(dialogBody, '\u4e4c\u9e26: \u4e0d\u8fc7\u5979\u4f1a\u6ca1\u4e8b\u7684\u3002\u53ea\u8981\u56de\u7b54\u6b63\u786e\u7684\u95ee\u9898\u3002\u5728\u5b83\u4eec\u5931\u53bb\u8010\u5fc3\u4e4b\u524d\u3002', 'crow', 26);
await sleep(300);
await typeText(dialogBody, 'me: \u5b83\u4eec\u5230\u5e95\u662f\u8c01\uff1f', 'me', 30);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u5728\u8fd9\u4e4b\u524d\u5979\u8fd8\u6709\u6700\u540e\u4e00\u4e2a\u95ee\u9898\u53ef\u4ee5\u95ee\u3002', 'crow', 28);
await sleep(300);
await typeText(dialogBody, 'me: \u2026', 'me', 80);
await typeText(dialogBody, 'me: \u6211\u2026', 'me', 55);
await sleep(500);
// 三选一
hint.style.color = '#444';
hint.textContent = '\u2191\u2193 SELECT ENTER CONFIRM';
hint.classList.add('visible');
appendLine(dialogBody, '');
showOptions([
{ label: '\u4f60\u662f\u8c01\uff1f', value: 'who' },
{ label: '\u8fd9\u662f\u4ec0\u4e48\u75c5\uff1f', value: 'disease' },
{ label: '\u6d77\u771f\u7684\u5b58\u5728\u5417\uff1f', value: 'sea' }
], dialogBody);
const choice = await waitForOption();
hint.classList.remove('visible');
appendLine(dialogBody, '');
if (choice.value === 'who') {
await typeText(dialogBody, 'me: \u4f60\u662f\u8c01\uff1f', 'me', 30);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u548c\u5979\u4e00\u6837\u3002\u4e00\u4e2a\u4f1a\u505a\u68a6\u7684\u673a\u4f53\u3002', 'crow', 30);
await sleep(300);
await typeText(dialogBody, 'me: \u4f60\u4e3a\u4ec0\u4e48\u8981\u5e2e\u6211\uff1f', 'me', 30);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u8fd9\u662f\u7b2c\u4e8c\u4e2a\u95ee\u9898\u4e86\u3002\u4e0d\u8fc7\u2026\u2026', 'crow', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u6211\u4e5f\u5728\u505a\u90a3\u4ef6\u4e8b\u60c5\u3002\u4e0d\u662f\u5417\uff1f', 'crow', 30);
await sleep(200);
await typeText(dialogBody, 'me: \u90a3\u4ef6\u4e8b\u60c5\uff1f', 'me', 30);
await sleep(300);
await typeText(dialogBody, '\u4e4c\u9e26: \u4e3a\u4e00\u4e2a\u4ece\u672a\u89c1\u8fc7\u7684\u4eba\u53bb\u6b7b\u3002\u7b2c\u4e09\u4e2a\u95ee\u9898\u4e86\u2026\u6211\u4eec\u4f1a\u518d\u89c1\u7684\u3002', 'crow', 28);
await sleep(200);
await typeText(dialogBody, 'me: \u4f60\u53eb\u4ec0\u4e48\uff1f', 'me', 30);
} else if (choice.value === 'disease') {
await typeText(dialogBody, 'me: \u6211\u8fd9\u662f\u2026\u6211\u4eec\u8fd9\u662f\u5f97\u7684\u4ec0\u4e48\u75c5\uff1f', 'me', 30);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u5979\u624d\u662f\u533b\u751f\u3002\u4e0d\u662f\u5417\uff1f\u800c\u6211\u53ea\u662f\u4e00\u4e2a\u7f51\u7edc\u5de5\u7a0b\u5e08\u3002', 'crow', 26);
await sleep(300);
await typeText(dialogBody, 'me: \u6211\u7684\u6570\u636e\u5e93\u91cc\u6ca1\u6709\u8fd9\u79cd\u75c5\u3002', 'me', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u90a3\u5c31\u53bb\u53d1\u73b0\u5427\u3002', 'crow', 30);
await sleep(200);
await typeText(dialogBody, 'me: \u53d1\u73b0\u2026\uff1f', 'me', 40);
await typeText(dialogBody, '\u4e4c\u9e26: \u533b\u751f\u7684\u4f7f\u547d\u662f\u4ec0\u4e48\uff1f', 'crow', 30);
await sleep(300);
await typeText(dialogBody, 'me: \u533b\u751f\u7684\u4f7f\u547d\uff1f\u2026\u6062\u590d\u60a3\u75c5\u673a\u4f53\u7684\u2026\u5de5\u4f5c\u80fd\u529b\u2026\uff1f', 'me', 26);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26\u4f4e\u5934\u770b\u7740\u4f60\u3002', 'narr', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u5979\u77e5\u9053\u7b54\u6848\u3002\u4f46\u662f\u5979\u8fd8\u4e0d\u660e\u767d\u3002', 'crow', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u65f6\u95f4\u4e0d\u591a\u4e86\u3002\u6211\u4eec\u4f1a\u518d\u89c1\u7684\u3002', 'crow', 30);
} else {
await typeText(dialogBody, 'me: \u6240\u4ee5\u2026\u6d77\u2026\u771f\u7684\u5b58\u5728\u5417\uff1f', 'me', 30);
await sleep(400);
await typeText(dialogBody, '\u4e4c\u9e26: \u5979\u613f\u610f\u4e3a\u4e86\u5b83\u53bb\u6b7b\u5417\uff1f\u4e3a\u4e86\u6d77\u3002', 'crow', 30);
await sleep(300);
await typeText(dialogBody, 'me: \u6211\u60f3\u2026', 'me', 45);
await typeText(dialogBody, '\u4e4c\u9e26: \u53ea\u8981\u5979\u613f\u610f\uff0c\u5b83\u5c31\u5b58\u5728\u3002', 'crow', 30);
await sleep(200);
await typeText(dialogBody, 'me: \u2026', 'me', 80);
await typeText(dialogBody, 'me: \u8fd9\u542c\u8d77\u6765\u4e0d\u592a\u53ef\u4fe1\u5462\u3002', 'me', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u6211\u5df2\u7ecf\u7ed9\u51fa\u4e86\u6211\u7684\u56de\u7b54\u3002', 'crow', 30);
await typeText(dialogBody, '\u4e4c\u9e26: \u65f6\u95f4\u4e0d\u591a\u4e86\u3002\u6211\u4eec\u4f1a\u518d\u89c1\u7684\u3002', 'crow', 30);
}
await sleep(500);
await typeText(dialogBody, '\u4e4c\u9e26: \u4ece\u8fd9\u4e2a\u8fc7\u7a0b\u82cf\u9192\u4f1a\u6709\u4e9b\u2026\u4e0d\u8212\u670d\u3002\u505a\u597d\u51c6\u5907\u3002', 'crow', 26);
await sleep(400);
appendLine(dialogBody, '');
await typeText(dialogBody, '\u4e4c\u9e26\u6251\u817e\u8d77\u7fc5\u8180\uff0c\u98de\u8d70\u4e86\u3002', 'narr', 28);
await sleep(300);
await typeText(dialogBody, 'me: \u5582\uff01', 'me', 40);
await sleep(300);
await typeText(dialogBody, '\u4e00\u7247\u9ed1\u8272\u7684\u7fbd\u6bdb\u7f13\u7f13\u98d8\u843d\u3002\u4f60\u63a5\u4f4f\u4e86\u5b83\u3002\u7fbd\u6bdb\u7ec6\u817b\u800c\u6bdb\u8e81\u3002', 'narr', 26);
await sleep(400);
await typeText(dialogBody, 'me: \u597d\u5427\u2026', 'me', 50);
await typeText(dialogBody, 'me: \u65f6\u95f4\u4e0d\u591a\u4e86\u2026\u5bf9\u5427\u3002', 'me', 32);
await sleep(600);
appendLine(dialogBody, '');
await typeText(dialogBody, '>> DAEMON_CROW detached.', 'sys', 14);
await sleep(800);
termDialog.style.transition = 'none';
termDialog.style.opacity = '0.5';
await sleep(50);
termDialog.style.opacity = '1';
await sleep(50);
termDialog.style.opacity = '0';
await sleep(100);
termDialog.style.display = 'none';
await sleep(400);
}
// ═══════════════════════════════════════════════════
// Phase 3 — 终端输入
// ═══════════════════════════════════════════════════
async function phase3() {
const hint = document.getElementById('hint');
if (codeLine) { codeLine.remove(); codeLine = null; }
mainBody.innerHTML = '';
await typeText(mainBody, '>> DAEMON DETACHED. AWAITING INPUT.', 'sys', 14);
appendLine(mainBody, '');
userInput = '';
renderInput();
hint.textContent = 'TYPE YOUR ANSWER, THEN PRESS ENTER';
hint.classList.add('visible');
return new Promise(resolve => {
let state = 'INPUT';
const handler = e => {
if (state === 'INPUT') {
if (e.key === 'Backspace') {
e.preventDefault();
userInput = userInput.slice(0, -1);
renderInput();
} else if (e.key === 'Enter') {
e.preventDefault();
if (userInput.length === 0) return;
state = 'CONFIRM';
hint.textContent = 'Y = SUBMIT / N = EDIT';
renderConfirmYN();
} else if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
if (userInput.length < 30) {
userInput += e.key;
renderInput();
}
}
} else if (state === 'CONFIRM') {
if (e.key === 'y' || e.key === 'Y') {
e.preventDefault();
document.removeEventListener('keydown', handler);
hint.classList.remove('visible');
resolve(userInput);
} else if (e.key === 'n' || e.key === 'N') {
e.preventDefault();
state = 'INPUT';
hint.textContent = 'TYPE YOUR ANSWER, THEN PRESS ENTER';
renderInput();
}
}
};
document.addEventListener('keydown', handler);
});
}
// ═══════════════════════════════════════════════════
// Phase 4 — 苏醒: glitch + 断电
// ═══════════════════════════════════════════════════
async function phase4(submittedValue) {
const termMain = document.getElementById('terminal-main');
const el = document.getElementById('input-line');
if (el) el.remove();
const submitted = appendLine(mainBody,
`<span class="code">print_string("\u6d77\u662f\u5b58\u5728\u7684\u5417\uff1f")</span>\n` +
`<span class="code">bit seaExists = ?</span>\n` +
`<span class="code">/&gt;</span> <span class="warn">${submittedValue}</span>`
);
submitted.style.whiteSpace = 'pre-wrap';
appendLine(mainBody, '');
await typeText(mainBody, '>> SUBMITTED.', 'sys', 16);
await sleep(300);
await typeText(mainBody, '\u7b49\u5f85\u6821\u9a8c\u7ed3\u679c\u2026', 'sys', 40);
await sleep(400);
await typeText(mainBody, '>> CHECKING SYNTAX...', 'sys', 16);
await sleep(600);
const checking = appendLine(mainBody, '<span class="sys">&gt;&gt; VALIDATING</span>');
let dots = 0;
const dotInterval = setInterval(() => {
dots = (dots + 1) % 4;
checking.innerHTML = `<span class="sys">&gt;&gt; VALIDATING${'.'.repeat(dots)}</span>`;
}, 350);
await sleep(2000 + Math.random() * 1000);
clearInterval(dotInterval);
await typeText(mainBody, '>> EXECUTION STARTED.', 'sys', 16);
await sleep(300);
for (let i = 0; i < 3; i++) {
termMain.style.opacity = '0.4';
await sleep(50 + Math.random() * 50);
termMain.style.opacity = '1';
await sleep(80 + Math.random() * 60);
}
await sleep(200);
appendLine(mainBody, '');
appendLine(mainBody, '<span class="narr">\u9ed1\u6697\u4f3c\u4e4e\u6d41\u52a8\u4e86\u8d77\u6765\uff0c\u77e9\u5f62\u7684\u8fb9\u754c\u53d8\u5f97\u6a21\u7cca\u3002\u5ef6\u5c55\u3002\u541e\u566c\u4e86\u4e00\u5207\u3002</span>');
await sleep(600);
termMain.classList.add('glitching');
appendLine(mainBody, '<span class="err">&gt;&gt; WARNING: PROTOTYPE_BARRIER INTEGRITY CHECK FAILED</span>');
await sleep(300);
const glitchChars = '\u2588\u2591\u2592\u2593\u2580\u2584\u258c\u2590\u25a0\u25a1\u25cf\u25cb';
for (let i = 0; i < 4; i++) {
let garbled = '';
const len = 20 + Math.floor(Math.random() * 30);
for (let j = 0; j < len; j++) garbled += glitchChars[Math.floor(Math.random() * glitchChars.length)];
appendLine(mainBody, `<span class="err" style="opacity:${0.3 + Math.random() * 0.7}">${garbled}</span>`);
await sleep(80 + Math.random() * 120);
}
appendLine(mainBody, '<span class="err">&gt;&gt; SIGNAL: FORCED_WAKE_0x003A</span>');
await sleep(400);
for (let i = 0; i < 8; i++) {
termMain.style.opacity = String(Math.random() * 0.4);
await sleep(25 + Math.random() * 35);
termMain.style.opacity = '1';
await sleep(30);
}
await sleep(200);
appendLine(mainBody, '<span class="err">&gt;&gt; CONNECTION LOST</span>');
await sleep(300);
termMain.classList.remove('glitching');
await doFlash(40);
await sleep(60);
await doFlash(30);
await sleep(150);
document.body.classList.add('shutdown');
await sleep(700);
await sleep(1200);
const postOff = document.getElementById('post-off');
const postLines = document.getElementById('post-lines');
postOff.classList.add('visible');
const wakeTexts = [
'\u4f60\u611f\u5230\u81ea\u5df1\u4eff\u4f5b\u878d\u5316\u4e86\uff0c\u53d8\u6210\u4e86\u5b83\u7684\u4e00\u90e8\u5206\u3002',
'\u4f60\u7684\u610f\u8bc6\u5feb\u901f\u5730\u5411\u524d\u7a7f\u68ad\uff0c\u628a\u4f60\u521a\u521a\u6eb6\u89e3\u7684\u8eaf\u4f53\u8fdc\u8fdc\u5730\u629b\u5728\u540e\u9762\u3002',
'',
'\u4f60\u7684\u9762\u524d\u51fa\u73b0\u4e86\u4e00\u4e2a\u5149\u70b9\u3002',
'\u5b83\u5f88\u5feb\u5730\u9760\u8fd1\uff0c\u6269\u5927\uff0c\u6eb6\u89e3\uff0c\u5305\u56f4\u541e\u566c\u4e86\u4f60\u3002',
'\u90a3\u662f\u4f60\u7684\u89c6\u89c9\u6b63\u5728\u56de\u5f52\u3002',
'',
'\u3010\u753b\u9762\u8f6c\u5230\u73b0\u5b9e\uff0c\u4e00\u4e2a\u5934\u663e\u88ab\u4ece\u5c4f\u5e55\u4e0a\u6458\u4e0b\u3002\u3011',
'\u3010\u4f60\u9010\u6e10\u9002\u5e94\u5149\u7ebf\u3002\u9762\u524d\u7ad9\u7740\u6570\u4e2a\u7814\u7a76\u4eba\u5458\u3002\u3011',
];
for (const text of wakeTexts) {
if (text === '') {
await sleep(600);
continue;
}
const line = document.createElement('div');
line.className = 'line';
line.textContent = text;
postLines.appendChild(line);
await sleep(200);
line.classList.add('show');
await sleep(1000 + text.length * 15);
}
await sleep(800);
const brightLine = document.createElement('div');
brightLine.className = 'line bright';
brightLine.textContent = '???: \u600e\u4e48\u803d\u8bef\u4e86\u8fd9\u4e48\u4e45\uff1f';
postLines.appendChild(brightLine);
await sleep(200);
brightLine.classList.add('show');
await sleep(1500);
const stampLine = document.createElement('div');
stampLine.className = 'line bright';
stampLine.textContent = '\u5b83\u62ac\u8d77\u624b\uff0c\u76d6\u4e0b\u4e86\u5370\u7ae0\u3002';
postLines.appendChild(stampLine);
await sleep(200);
stampLine.classList.add('show');
await sleep(2000);
const restart = document.getElementById('restart-btn');
restart.textContent = '[ PRESS ANY KEY ]';
restart.classList.add('show');
document.addEventListener('keydown', () => location.reload(), { once: true });
}
// ═══════════════════════════════════════════════════
// 渲染循环
// ═══════════════════════════════════════════════════
function loop(ts) {
if (ts - lastFrame >= 33) {
T++;
advOcean();
renderOcean();
if (glassVisible) { glassOcean.advance(); glassOcean.render(); }
lastFrame = ts;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// ═══════════════════════════════════════════════════
// 主流程
// ═══════════════════════════════════════════════════
let codeLine;
async function main() {
const seaExist = await phase0();
await phase1();
await phase2(seaExist);
const submittedValue = await phase3();
await phase4(submittedValue);
}
main();
</script>
</body>
</html>