/* 第 3 页场景数据 前景:演职员表;背景:电线、麻雀、草。 本页不铺「天」,空白格保持为空。 */ (() => { /* 固定种子的噪声,保证每次渲染一致 */ const noise = (x, y) => { let h = Math.imul(x, 374761393) + Math.imul(y, 668265263); h = Math.imul(h ^ (h >>> 13), 1274126177); return ((h ^ (h >>> 16)) >>> 0) / 4294967296; }; /* 火山:占据下半屏,下 2/3 为山体、上 1/3 为熔岩棋盘格 */ const volcano = []; const VOLCANO_TOP = 21; const VOLCANO_BOTTOM = 42; /* 每列的熔岩下沿,形成不规则的熔岩流 */ const lavaBottom = Array.from({ length: 28 }, (_, x) => { return VOLCANO_TOP + 6 + Math.floor(noise(x, 3) * 3); }); [5, 13, 21].forEach((x) => { lavaBottom[x] += 4 + Math.floor(noise(x, 9) * 3); }); /* 两层熔岩的分界:按列小幅随机起伏 */ const lavaSplit = Array.from({ length: 28 }, (_, x) => { const offset = Math.round((noise(x, 13) - 0.5) * 4); return Math.max( VOLCANO_TOP + 2, Math.min(VOLCANO_TOP + 5, VOLCANO_TOP + 3 + offset) ); }); /* 主熔岩区以下为山体,两侧做对称平滑处理 */ const ROCK_START = VOLCANO_TOP + 7; for (let y = VOLCANO_TOP; y <= VOLCANO_BOTTOM; y++) { /* 距底部 5 格起铺满整幅宽度,避免山体过陡 */ const fullWidthAt = VOLCANO_BOTTOM - 5; const t = (y - VOLCANO_TOP) / (fullWidthAt - VOLCANO_TOP); const baseWidth = y >= fullWidthAt ? 28 : Math.round(10 + 18 * Math.min(1, Math.max(0, t))); let width; let left; if (y < ROCK_START) { /* 熔岩区保持原有随机形状 */ const jitter = y >= fullWidthAt ? 0 : Math.round((noise(0, y) - 0.5) * 2); width = Math.max(4, Math.min(28, baseWidth + jitter)); left = Math.floor((28 - width) / 2); left += Math.round((noise(1, y) - 0.5) * 2); left = Math.max(0, Math.min(28 - width, left)); if (y === VOLCANO_TOP + 1) left = Math.max(0, left - 1); } else { /* 山体左右对称,宽度取偶数和,保留低频小幅随机 */ const wobble = Math.round((noise(7, Math.floor(y / 3)) - 0.5) * 2); let w = y >= fullWidthAt ? 28 : Math.min(28, baseWidth + wobble); w -= w % 2; width = Math.max(4, w); left = (28 - width) / 2; } /* 顶部火山口:仅两行凹下,第一行缺口更宽 */ const craterGap = y < ROCK_START ? [4, 2][y - VOLCANO_TOP] || 0 : 0; const gapStart = craterGap ? Math.floor((width - craterGap) / 2) : 0; for (let i = 0; i < width; i++) { if (craterGap && i >= gapStart && i < gapStart + craterGap) continue; const x = left + i; const isLava = y <= lavaBottom[x]; const isUpperLava = isLava && y <= lavaSplit[x]; volcano.push({ text: isLava ? ((x + y) % 2 === 0 ? '熔' : '岩') : '石', x, y, cls: isLava ? (isUpperLava ? 'lava-a' : 'lava-b') : 'rock', }); } } /* 第十行左侧补一个「熔」 */ const row10 = volcano.filter((cell) => cell.y === VOLCANO_TOP + 9); const row10Left = Math.min(...row10.map((cell) => cell.x)); volcano.push({ text: '熔', x: row10Left - 1, y: VOLCANO_TOP + 9, cls: 'lava-b', }); TextScene.render({ cols: 28, rows: 44, skyChar: '', glyphs: [ /* 电线:整行贯穿页面 */ { text: '电线'.repeat(14), x: 0, y: 8, cls: 'wire' }, /* 麻雀:竖排于从左到右第四格,落在电线上方 */ { text: '麻雀', x: 3, y: 6, cls: 'sparrow', vertical: true }, /* 火山铺在底部背景,草、消防栓在其上层 */ ...volcano, /* 草:最底部一排 */ { text: '草'.repeat(28), x: 0, y: 43, cls: 'grass' }, /* 消防栓:最后三行分散竖排九根,左侧留 2 格 */ ...[2, 5, 8, 10, 13, 16, 19, 23, 25].map((x) => ({ text: '消防栓', x, y: 41, cls: 'hydrant', vertical: true, })), ], }); const scene = document.querySelector('.scene'); const credits = document.createElement('h1'); credits.className = 'credits'; credits.textContent = '演职员表'; scene.append(credits); /* 36 个 nametag:4 列 × 9 行,补满 */ const NAMES = [ '郑滕飞', '秦雪莹', '关宸宇', '王天晓', '谢雨舟', '储明威', '庞熙若', '刘婉莹', '陈彦淇', '古雅文', '周胤伯', '郑佳欣', '娄敬庭', '张加弛', '李昀芷', '吴红霓', '鲍馨仪', '刘润泽', '李若熙', '马希予', '袁立桓', '蒋子模', '李有睿', '王海心', '朱丽烨', '朱彦阳', '汪楚钧', '吴玥', '张梓轩', '罗午阳', '郭柯言', '王之饶', '应玥', '湛茜茜', '黄钰涵', '虞韫嘉', ]; const TAG = { cols: 4, rows: 9, width: 5, height: 2, gapX: 1, gapY: 1, left: 2, top: 12, }; let nameIndex = 0; for (let row = 0; row < TAG.rows; row++) { for (let col = 0; col < TAG.cols; col++) { const name = NAMES[nameIndex++]; if (!name) break; const tag = document.createElement('div'); tag.className = 'nametag'; tag.style.left = `${TAG.left + col * (TAG.width + TAG.gapX)}em`; tag.style.top = `${TAG.top + row * (TAG.height + TAG.gapY)}em`; const label = document.createElement('span'); label.className = 'nametag-name'; label.textContent = name; tag.append(label); scene.append(tag); } } })();