1332 lines
50 KiB
HTML
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 ? '>' : '\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 ? '>' : '\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">/></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">/></span> <span class="warn">${userInput}</span>\n\n` +
|
|
`<span class="sys">>> 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">/></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">/></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">>> VALIDATING</span>');
|
|
let dots = 0;
|
|
const dotInterval = setInterval(() => {
|
|
dots = (dots + 1) % 4;
|
|
checking.innerHTML = `<span class="sys">>> 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">>> 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">>> 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">>> 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>
|