Add Kimi K3 (Kimi Web Max) mythos-craft run
One-shot, unmodified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4b2a9e0173
commit
f2919d801b
2 files changed
+871
No files matched your search
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"label": "Kimi K3",
|
||||
"vendor": "Moonshot AI",
|
||||
"harness": "Kimi Web",
|
||||
"effort": "Max",
|
||||
"artifactDir": "kimi-k3/kimi-web-max",
|
||||
"order": 97.9,
|
||||
"runs": {
|
||||
"mythos-craft": {
|
||||
"note": {
|
||||
"zh": "在 Kimi 网页版使用 Kimi K3(Max 档位)一次生成,未进行任何修改。",
|
||||
"en": "Generated in one shot in the Kimi web app on Kimi K3 (Max), unmodified."
|
||||
},
|
||||
"contributor": "Nagi-ovo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>神话大陆 · MythCraft — 我的世界幻想克隆</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html,body { width:100%; height:100%; overflow:hidden; background:#000; font-family:"Segoe UI","Microsoft YaHei",sans-serif; }
|
||||
#game { position:fixed; inset:0; display:block; }
|
||||
/* 十字准星 */
|
||||
#crosshair { position:fixed; left:50%; top:50%; width:22px; height:22px; transform:translate(-50%,-50%); pointer-events:none; z-index:10; mix-blend-mode:difference; }
|
||||
#crosshair::before,#crosshair::after { content:""; position:absolute; background:#fff; }
|
||||
#crosshair::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
|
||||
#crosshair::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }
|
||||
/* 快捷栏 */
|
||||
#hotbar { position:fixed; bottom:14px; left:50%; transform:translateX(-50%); display:flex; gap:4px; z-index:10; pointer-events:none; }
|
||||
.slot { width:52px; height:52px; border:3px solid rgba(0,0,0,.55); border-radius:4px; background:rgba(20,20,20,.45); position:relative; box-shadow:inset 0 0 0 2px rgba(255,255,255,.15); }
|
||||
.slot.sel { border-color:#fff; box-shadow:0 0 12px rgba(255,255,255,.7), inset 0 0 0 2px rgba(255,255,255,.4); }
|
||||
.slot img { width:100%; height:100%; image-rendering:pixelated; border-radius:2px; }
|
||||
.slot .num { position:absolute; top:1px; left:4px; font-size:11px; color:#fff; text-shadow:1px 1px 0 #000; }
|
||||
.slot .name { position:absolute; bottom:-20px; left:50%; transform:translateX(-50%); font-size:11px; color:#fff; text-shadow:1px 1px 0 #000; white-space:nowrap; display:none; }
|
||||
.slot.sel .name { display:block; }
|
||||
/* 信息面板 */
|
||||
#stats { position:fixed; top:10px; left:10px; z-index:10; color:#fff; font-size:12px; text-shadow:1px 1px 0 #000; background:rgba(0,0,0,.35); padding:8px 12px; border-radius:6px; line-height:1.7; pointer-events:none; white-space:pre; }
|
||||
#hint { position:fixed; top:10px; right:10px; z-index:10; color:#eee; font-size:12px; text-shadow:1px 1px 0 #000; background:rgba(0,0,0,.35); padding:8px 12px; border-radius:6px; line-height:1.8; pointer-events:none; text-align:right; }
|
||||
/* 开始遮罩 */
|
||||
#overlay { position:fixed; inset:0; z-index:100; display:flex; flex-direction:column; align-items:center; justify-content:center; background:radial-gradient(ellipse at center, rgba(20,30,60,.88), rgba(0,0,0,.96)); cursor:pointer; transition:opacity .3s; }
|
||||
#overlay.hidden { opacity:0; pointer-events:none; }
|
||||
#overlay h1 { font-size:52px; color:#f5e6c8; letter-spacing:8px; text-shadow:0 0 24px rgba(255,200,100,.5), 3px 3px 0 #3a2a10; margin-bottom:6px; }
|
||||
#overlay h2 { font-size:16px; color:#8fd3ff; font-weight:400; letter-spacing:4px; margin-bottom:34px; }
|
||||
#overlay .panel { background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.15); border-radius:10px; padding:22px 34px; color:#ddd; font-size:14px; line-height:2.1; }
|
||||
#overlay .panel b { color:#ffd76a; display:inline-block; min-width:86px; }
|
||||
#overlay .go { margin-top:30px; font-size:20px; color:#fff; padding:12px 46px; border:2px solid #ffd76a; border-radius:8px; background:rgba(255,215,106,.12); letter-spacing:6px; animation:pulse 1.6s infinite; }
|
||||
@keyframes pulse { 0%,100%{box-shadow:0 0 8px rgba(255,215,106,.3);} 50%{box-shadow:0 0 26px rgba(255,215,106,.8);} }
|
||||
#loading { position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center; background:#0a0f1e; color:#9fc6ff; font-size:18px; letter-spacing:4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game"></canvas>
|
||||
<div id="crosshair"></div>
|
||||
<div id="hotbar"></div>
|
||||
<div id="stats"></div>
|
||||
<div id="hint">WASD 移动 · 空格 跳跃 · Shift 疾跑<br>F 飞行开关 · 左键 破坏 · 右键 放置<br>1-9 / 滚轮 切换方块 · Esc 释放鼠标</div>
|
||||
<div id="overlay">
|
||||
<h1>神话大陆</h1>
|
||||
<h2>MYTHCRAFT · 幻想方块世界</h2>
|
||||
<div class="panel">
|
||||
<div><b>W A S D</b>移动 <b>鼠标</b>环视 <b>空格</b>跳跃</div>
|
||||
<div><b>Shift</b>疾跑 <b>F</b>飞行模式(空格升 / Shift 降)</div>
|
||||
<div><b>左键</b>破坏方块 <b>右键</b>放置方块</div>
|
||||
<div><b>1 ~ 9 / 滚轮</b>切换手持方块</div>
|
||||
<div style="margin-top:10px;color:#9fc6ff">探索世界中的远古遗迹、符文石阵、浮空岛、水晶尖塔、巫师塔、巨灵古树与黑曜石传送门……</div>
|
||||
</div>
|
||||
<div class="go">点 击 进 入 世 界</div>
|
||||
</div>
|
||||
<div id="loading">正在生成神话大陆……</div>
|
||||
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js" } }
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
|
||||
/* ============================================================
|
||||
0. 基础常量与工具
|
||||
============================================================ */
|
||||
const CHUNK = 16; // 区块边长
|
||||
const WORLD_H = 112; // 世界高度
|
||||
const SEA = 32; // 海平面
|
||||
const RENDER_DIST = 4; // 渲染半径(区块)
|
||||
const SEED = 20260716;
|
||||
|
||||
// 方块 ID
|
||||
const AIR=0, GRASS=1, DIRT=2, STONE=3, SAND=4, LOG=5, LEAVES=6, PLANKS=7,
|
||||
MARBLE=8, OBSIDIAN=9, CRYSTAL=10, RUNE=11, GOLD=12, BRICK=13,
|
||||
LAVA=14, PORTAL=15, SANDSTONE=16, WATER=17, BEDROCK=18;
|
||||
|
||||
// 确定性 2D 哈希 -> [0,1)
|
||||
function hash2(x, z) {
|
||||
let n = (x * 374761393 + z * 668265263 + SEED * 974634) | 0;
|
||||
n = Math.imul(n ^ (n >>> 13), 1274126177);
|
||||
return ((n ^ (n >>> 16)) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
1. Simplex 噪声(种子化)+ 分形噪声
|
||||
============================================================ */
|
||||
const F2 = 0.5 * (Math.sqrt(3) - 1), G2 = (3 - Math.sqrt(3)) / 6;
|
||||
const perm = new Uint8Array(512);
|
||||
{
|
||||
const p = new Uint8Array(256);
|
||||
for (let i = 0; i < 256; i++) p[i] = i;
|
||||
let s = SEED;
|
||||
const rnd = () => (s = (s * 9301 + 49297) % 233280) / 233280;
|
||||
for (let i = 255; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); const t = p[i]; p[i] = p[j]; p[j] = t; }
|
||||
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
|
||||
}
|
||||
const grad2 = [1,1,-1,1,1,-1,-1,-1,1,0,-1,0,0,1,0,-1];
|
||||
function snoise(xin, yin) {
|
||||
let n0 = 0, n1 = 0, n2 = 0;
|
||||
const sk = (xin + yin) * F2;
|
||||
const i = Math.floor(xin + sk), j = Math.floor(yin + sk);
|
||||
const t = (i + j) * G2;
|
||||
const x0 = xin - (i - t), y0 = yin - (j - t);
|
||||
const i1 = x0 > y0 ? 1 : 0, j1 = x0 > y0 ? 0 : 1;
|
||||
const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
|
||||
const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
|
||||
const ii = i & 255, jj = j & 255;
|
||||
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||
if (t0 >= 0) { const g = (perm[ii + perm[jj]] & 7) * 2; t0 *= t0; n0 = t0 * t0 * (grad2[g] * x0 + grad2[g + 1] * y0); }
|
||||
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||
if (t1 >= 0) { const g = (perm[ii + i1 + perm[jj + j1]] & 7) * 2; t1 *= t1; n1 = t1 * t1 * (grad2[g] * x1 + grad2[g + 1] * y1); }
|
||||
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||
if (t2 >= 0) { const g = (perm[ii + 1 + perm[jj + 1]] & 7) * 2; t2 *= t2; n2 = t2 * t2 * (grad2[g] * x2 + grad2[g + 1] * y2); }
|
||||
return 70 * (n0 + n1 + n2);
|
||||
}
|
||||
function fbm(x, y, oct) {
|
||||
let v = 0, amp = 0.5, f = 1, tot = 0;
|
||||
for (let o = 0; o < oct; o++) { v += amp * snoise(x * f, y * f); tot += amp; amp *= 0.5; f *= 2.03; }
|
||||
return v / tot;
|
||||
}
|
||||
const smoothstep = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
|
||||
|
||||
/* 地形高度:大陆 + 丘陵 + 山脊 */
|
||||
function terrainHeight(wx, wz) {
|
||||
const cont = fbm(wx * 0.0035, wz * 0.0035, 4);
|
||||
const hills = fbm(wx * 0.018 + 37.2, wz * 0.018 + 11.7, 4);
|
||||
const mask = smoothstep(0.12, 0.55, fbm(wx * 0.006 + 500, wz * 0.006 - 500, 3));
|
||||
const ridge = 1 - Math.abs(fbm(wx * 0.012 + 900, wz * 0.012 + 321, 4));
|
||||
let h = 36 + cont * 20 + hills * 9 + mask * ridge * ridge * 44;
|
||||
return Math.max(4, Math.min(90, Math.floor(h)));
|
||||
}
|
||||
/* 魔力生态域(决定晶簇等地物密度) */
|
||||
function magicField(wx, wz) { return fbm(wx * 0.008 - 2000, wz * 0.008 + 2000, 3); }
|
||||
/* 森林覆盖度 */
|
||||
function forestField(wx, wz) { return fbm(wx * 0.011 + 77, wz * 0.011 - 33, 3); }
|
||||
|
||||
/* ============================================================
|
||||
2. 程序化像素材质(16x16,还原 Minecraft 质感)
|
||||
============================================================ */
|
||||
let texSeed = 7;
|
||||
const trnd = () => (texSeed = (texSeed * 9301 + 49297) % 233280) / 233280;
|
||||
function makeCanvas(draw) {
|
||||
const c = document.createElement('canvas'); c.width = c.height = 16;
|
||||
const g = c.getContext('2d'); draw(g); return c;
|
||||
}
|
||||
function jitter(base, amt) {
|
||||
const r = Math.max(0, Math.min(255, base[0] + (trnd() * 2 - 1) * amt));
|
||||
const g = Math.max(0, Math.min(255, base[1] + (trnd() * 2 - 1) * amt));
|
||||
const b = Math.max(0, Math.min(255, base[2] + (trnd() * 2 - 1) * amt));
|
||||
return `rgb(${r | 0},${g | 0},${b | 0})`;
|
||||
}
|
||||
function fillNoise(g, base, amt) {
|
||||
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) { g.fillStyle = jitter(base, amt); g.fillRect(x, y, 1, 1); }
|
||||
}
|
||||
function speckle(g, color, n) {
|
||||
g.fillStyle = color;
|
||||
for (let i = 0; i < n; i++) g.fillRect((trnd() * 16) | 0, (trnd() * 16) | 0, 1, 1);
|
||||
}
|
||||
const TEX_DRAW = [
|
||||
/* 0 草顶 */ g => { fillNoise(g, [106, 170, 64], 20); speckle(g, 'rgb(86,140,50)', 22); speckle(g, 'rgb(130,196,84)', 14); },
|
||||
/* 1 草侧 */ g => { fillNoise(g, [134, 96, 67], 16); for (let x = 0; x < 16; x++) { const d = 3 + ((trnd() * 3) | 0); for (let y = 0; y < d; y++) { g.fillStyle = jitter([106, 170, 64], 18); g.fillRect(x, y, 1, 1); } } },
|
||||
/* 2 泥土 */ g => { fillNoise(g, [134, 96, 67], 16); speckle(g, 'rgb(110,76,52)', 18); speckle(g, 'rgb(158,120,88)', 10); },
|
||||
/* 3 石头 */ g => { fillNoise(g, [136, 136, 136], 10); speckle(g, 'rgb(112,112,112)', 20); speckle(g, 'rgb(156,156,156)', 10); },
|
||||
/* 4 沙子 */ g => { fillNoise(g, [219, 207, 163], 10); speckle(g, 'rgb(200,186,140)', 16); },
|
||||
/* 5 原木侧 */ g => { fillNoise(g, [107, 85, 50], 10); for (let x = 0; x < 16; x += 3) { g.fillStyle = 'rgb(84,64,36)'; for (let y = 0; y < 16; y++) if (trnd() < 0.8) g.fillRect(x, y, 1, 1); } },
|
||||
/* 6 原木顶 */ g => { fillNoise(g, [177, 142, 90], 8); g.strokeStyle = 'rgb(120,92,52)'; for (let r = 2; r < 9; r += 2) g.strokeRect(8 - r, 8 - r, r * 2, r * 2); },
|
||||
/* 7 树叶 */ g => { fillNoise(g, [58, 122, 40], 26); speckle(g, 'rgb(30,70,20)', 40); speckle(g, 'rgb(90,160,66)', 16); },
|
||||
/* 8 木板 */ g => { fillNoise(g, [170, 136, 82], 8); g.fillStyle = 'rgb(120,92,52)'; for (let y = 3; y < 16; y += 4) g.fillRect(0, y, 16, 1); for (let y = 0; y < 16; y += 4) g.fillRect(((y / 4) % 2) * 8 + 4, y, 1, 4); },
|
||||
/* 9 大理石 */ g => { fillNoise(g, [233, 231, 224], 6); g.strokeStyle = 'rgba(180,180,190,.7)'; g.beginPath(); g.moveTo(2, 0); g.lineTo(6, 8); g.lineTo(4, 16); g.moveTo(11, 0); g.lineTo(9, 9); g.lineTo(13, 16); g.stroke(); },
|
||||
/* 10 黑曜石*/ g => { fillNoise(g, [24, 18, 38], 8); speckle(g, 'rgb(72,50,122)', 16); speckle(g, 'rgb(10,8,18)', 12); },
|
||||
/* 11 水晶 */ g => { fillNoise(g, [110, 226, 240], 18); g.strokeStyle = 'rgb(220,255,255)'; g.beginPath(); g.moveTo(3, 13); g.lineTo(8, 2); g.lineTo(13, 13); g.moveTo(8, 2); g.lineTo(8, 15); g.stroke(); speckle(g, 'rgb(255,255,255)', 8); },
|
||||
/* 12 符文石*/ g => { fillNoise(g, [44, 44, 62], 8); g.fillStyle = 'rgb(190,110,255)'; const R = [[3,3],[4,3],[5,3],[4,4],[4,5],[3,6],[5,6],[10,4],[11,4],[10,5],[11,6],[10,7],[12,5],[7,10],[8,10],[7,11],[8,12],[9,11],[7,13],[8,13]]; for (const p of R) g.fillRect(p[0], p[1], 1, 1); },
|
||||
/* 13 金块 */ g => { fillNoise(g, [250, 210, 60], 14); g.fillStyle = 'rgb(200,150,20)'; g.fillRect(0, 0, 16, 1); g.fillRect(0, 15, 16, 1); g.fillRect(0, 0, 1, 16); g.fillRect(15, 0, 1, 16); speckle(g, 'rgb(255,240,160)', 10); },
|
||||
/* 14 石砖 */ g => { fillNoise(g, [124, 124, 124], 8); g.fillStyle = 'rgb(88,88,88)'; for (let y = 3; y < 16; y += 4) g.fillRect(0, y, 16, 1); for (let y = 0; y < 16; y += 4) { const off = ((y / 4) % 2) * 8; g.fillRect(off, y, 1, 4); g.fillRect((off + 8) % 16, y, 1, 4); } },
|
||||
/* 15 岩浆 */ g => { fillNoise(g, [255, 110, 20], 26); speckle(g, 'rgb(255,205,60)', 22); speckle(g, 'rgb(190,50,10)', 18); },
|
||||
/* 16 传送门*/ g => { fillNoise(g, [150, 60, 220], 34); speckle(g, 'rgb(230,170,255)', 18); speckle(g, 'rgb(90,20,160)', 22); },
|
||||
/* 17 水 */ g => { fillNoise(g, [52, 112, 222], 12); g.fillStyle = 'rgba(140,190,255,.6)'; for (let i = 0; i < 6; i++) g.fillRect((trnd() * 14) | 0, (trnd() * 16) | 0, 3, 1); },
|
||||
/* 18 基岩 */ g => { fillNoise(g, [64, 64, 64], 34); speckle(g, 'rgb(20,20,20)', 26); },
|
||||
/* 19 砂岩 */ g => { fillNoise(g, [216, 202, 155], 6); g.fillStyle = 'rgb(190,172,120)'; g.fillRect(0, 3, 16, 1); g.fillRect(0, 12, 16, 1); },
|
||||
];
|
||||
// 每个方块的 [top, side, bottom] 材质 ID
|
||||
const BLOCK_TEX = {
|
||||
[GRASS]: [0, 1, 2], [DIRT]: [2, 2, 2], [STONE]: [3, 3, 3], [SAND]: [4, 4, 4],
|
||||
[LOG]: [6, 5, 6], [LEAVES]: [7, 7, 7], [PLANKS]: [8, 8, 8], [MARBLE]: [9, 9, 9],
|
||||
[OBSIDIAN]: [10, 10, 10], [CRYSTAL]: [11, 11, 11], [RUNE]: [12, 12, 12], [GOLD]: [13, 13, 13],
|
||||
[BRICK]: [14, 14, 14], [LAVA]: [15, 15, 15], [PORTAL]: [16, 16, 16],
|
||||
[SANDSTONE]: [19, 19, 4], [WATER]: [17, 17, 17], [BEDROCK]: [18, 18, 18],
|
||||
};
|
||||
const texCanvases = TEX_DRAW.map(makeCanvas);
|
||||
const MATERIALS = texCanvases.map((c, i) => {
|
||||
const t = new THREE.CanvasTexture(c);
|
||||
t.magFilter = THREE.NearestFilter; t.minFilter = THREE.NearestFilter;
|
||||
t.colorSpace = THREE.SRGBColorSpace;
|
||||
const opt = { map: t, vertexColors: true };
|
||||
if (i === 17) { opt.transparent = true; opt.opacity = 0.72; opt.depthWrite = false; }
|
||||
const m = new THREE.MeshLambertMaterial(opt);
|
||||
if (i === 11) { m.emissive = new THREE.Color(0x33ddff); m.emissiveMap = t; m.emissiveIntensity = 0.85; }
|
||||
if (i === 12) { m.emissive = new THREE.Color(0xaa55ff); m.emissiveMap = t; m.emissiveIntensity = 0.9; }
|
||||
if (i === 13) { m.emissive = new THREE.Color(0x553d00); m.emissiveMap = t; m.emissiveIntensity = 0.4; }
|
||||
if (i === 15) { m.emissive = new THREE.Color(0xff7722); m.emissiveMap = t; m.emissiveIntensity = 1.0; }
|
||||
if (i === 16) { m.emissive = new THREE.Color(0xcc66ff); m.emissiveMap = t; m.emissiveIntensity = 1.0; }
|
||||
return m;
|
||||
});
|
||||
const BLOCK_NAMES = { [GRASS]: '草方块', [DIRT]: '泥土', [STONE]: '石头', [SAND]: '沙子', [LOG]: '原木', [PLANKS]: '木板', [MARBLE]: '大理石', [CRYSTAL]: '魔晶', [OBSIDIAN]: '黑曜石' };
|
||||
const isSolid = id => id !== AIR && id !== WATER;
|
||||
|
||||
/* ============================================================
|
||||
3. 区块数据与世界生成(地形 + 树木 + 神话建筑)
|
||||
============================================================ */
|
||||
const chunks = new Map(); // "cx,cz" -> {data, mesh, dirty}
|
||||
const ckey = (cx, cz) => cx + ',' + cz;
|
||||
const cidx = (x, y, z) => (y * CHUNK + z) * CHUNK + x;
|
||||
|
||||
function getBlock(wx, wy, wz) {
|
||||
if (wy < 0) return BEDROCK;
|
||||
if (wy >= WORLD_H) return AIR;
|
||||
const c = chunks.get(ckey(Math.floor(wx / CHUNK), Math.floor(wz / CHUNK)));
|
||||
if (!c) return AIR;
|
||||
return c.data[cidx(wx - Math.floor(wx / CHUNK) * CHUNK, wy, wz - Math.floor(wz / CHUNK) * CHUNK)];
|
||||
}
|
||||
|
||||
/* 树木:确定性判断某列是否有树,返回树干高(0 表示无树) */
|
||||
function treeAt(wx, wz, h) {
|
||||
if (h <= SEA + 1) return 0;
|
||||
const f = forestField(wx, wz);
|
||||
if (f < 0.08) return 0;
|
||||
const r = hash2(wx, wz);
|
||||
if (r < f * 0.035) return 4 + Math.floor(hash2(wx + 5, wz - 5) * 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- 神话建筑(结构)系统 ----------
|
||||
每 5x5 区块为一个“秘境区域”,按哈希确定性决定是否生成及种类 */
|
||||
const REGION = 80;
|
||||
function getStructure(rx, rz) {
|
||||
if (hash2(rx * 3 + 11, rz * 7 + 5) < 0.5) return null; // 50% 区域有秘境
|
||||
const type = 1 + Math.floor(hash2(rx + 31, rz + 17) * 8); // 1~8
|
||||
const ox = rx * REGION + 24 + Math.floor(hash2(rx + 7, rz + 3) * 32);
|
||||
const oz = rz * REGION + 24 + Math.floor(hash2(rx + 13, rz + 29) * 32);
|
||||
const seed = hash2(rx * 5 + 1, rz * 11 + 7);
|
||||
const oy = type === 4 ? 74 + Math.floor(seed * 12) : terrainHeight(ox, oz);
|
||||
return { type, ox, oy, oz, seed };
|
||||
}
|
||||
const STRUCTS = {
|
||||
/* 1. 符文石阵:环形大理石柱 + 发光符文 */
|
||||
1(put, clr, ox, oy, oz, rnd) {
|
||||
for (let dx = -9; dx <= 9; dx++) for (let dz = -9; dz <= 9; dz++) {
|
||||
const d = Math.sqrt(dx * dx + dz * dz);
|
||||
if (d <= 8.5) { put(ox + dx, oy, oz + dz, BRICK); for (let y = 1; y <= 4; y++) clr(ox + dx, oy + y, oz + dz); }
|
||||
if (d <= 6.5) for (let y = 1; y <= 5; y++) put(ox + dx, oy - y, oz + dz, STONE);
|
||||
}
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const a = k / 8 * Math.PI * 2, px = ox + Math.round(Math.cos(a) * 6), pz = oz + Math.round(Math.sin(a) * 6);
|
||||
const h = rnd() < 0.25 ? 2 + (rnd() * 2 | 0) : 5 + (rnd() * 3 | 0); // 部分断柱
|
||||
for (let y = 1; y <= h; y++) put(px, oy + y, pz, MARBLE);
|
||||
if (h >= 5) put(px, oy + h + 1, pz, RUNE);
|
||||
}
|
||||
for (let dx = -1; dx <= 1; dx++) for (let dz = -1; dz <= 1; dz++) put(ox + dx, oy + 1, oz + dz, MARBLE);
|
||||
put(ox, oy + 2, oz, GOLD); put(ox, oy + 3, oz, RUNE);
|
||||
},
|
||||
/* 2. 远古神殿遗迹 */
|
||||
2(put, clr, ox, oy, oz, rnd) {
|
||||
const W = 7, D = 6;
|
||||
for (let dx = -W; dx <= W; dx++) for (let dz = -D; dz <= D; dz++) {
|
||||
put(ox + dx, oy, oz + dz, MARBLE);
|
||||
if (Math.abs(dx) <= W - 1 && Math.abs(dz) <= D - 1) put(ox + dx, oy + 1, oz + dz, MARBLE);
|
||||
for (let y = 2; y <= 8; y++) clr(ox + dx, oy + y, oz + dz);
|
||||
for (let y = 1; y <= 6; y++) put(ox + dx, oy - y, oz + dz, STONE);
|
||||
}
|
||||
const cols = [[-W + 1, -D + 1], [W - 1, -D + 1], [-W + 1, D - 1], [W - 1, D - 1], [0, -D + 1], [0, D - 1]];
|
||||
for (const [cx0, cz0] of cols) {
|
||||
const h = rnd() < 0.3 ? 2 + (rnd() * 3 | 0) : 7;
|
||||
for (let y = 2; y <= 1 + h; y++) put(ox + cx0, oy + y, oz + cz0, MARBLE);
|
||||
if (h === 7) { put(ox + cx0, oy + 9, oz + cz0, GOLD); }
|
||||
}
|
||||
// 残破顶檐
|
||||
for (let dx = -W + 1; dx <= W - 1; dx++) for (let dz = -D + 1; dz <= D - 1; dz++)
|
||||
if ((Math.abs(dx) === W - 1 || Math.abs(dz) === D - 1) && rnd() < 0.55) put(ox + dx, oy + 9, oz + dz, MARBLE);
|
||||
// 火盆
|
||||
for (const [bx, bz] of [[-3, 0], [3, 0]]) { put(ox + bx, oy + 2, oz + bz, STONE); put(ox + bx, oy + 3, oz + bz, LAVA); }
|
||||
put(ox, oy + 2, oz, GOLD);
|
||||
},
|
||||
/* 3. 水晶尖塔 */
|
||||
3(put, clr, ox, oy, oz, rnd) {
|
||||
const H = 20 + (rnd() * 6 | 0);
|
||||
for (let y = 0; y < H; y++) {
|
||||
const r = Math.max(0.6, 4 * (1 - y / (H + 4)));
|
||||
for (let dx = -4; dx <= 4; dx++) for (let dz = -4; dz <= 4; dz++) {
|
||||
const d = Math.sqrt(dx * dx + dz * dz);
|
||||
if (d <= r && d >= r - 1.3) put(ox + dx, oy + y, oz + dz, OBSIDIAN);
|
||||
if (y < 3 && d < r - 1.3) put(ox + dx, oy + y, oz + dz, OBSIDIAN);
|
||||
}
|
||||
}
|
||||
// 顶部晶簇
|
||||
put(ox, oy + H, oz, CRYSTAL); put(ox, oy + H + 1, oz, CRYSTAL); put(ox, oy + H + 2, oz, CRYSTAL);
|
||||
put(ox + 1, oy + H, oz, CRYSTAL); put(ox - 1, oy + H, oz, CRYSTAL);
|
||||
put(ox, oy + H, oz + 1, CRYSTAL); put(ox, oy + H, oz - 1, CRYSTAL);
|
||||
put(ox, oy + H + 3, oz, RUNE);
|
||||
// 环绕浮晶
|
||||
for (let k = 0; k < 5; k++) {
|
||||
const a = rnd() * Math.PI * 2, r = 4 + rnd() * 3, fy = oy + H - 4 - (rnd() * 8 | 0);
|
||||
put(ox + Math.round(Math.cos(a) * r), fy, oz + Math.round(Math.sin(a) * r), CRYSTAL);
|
||||
}
|
||||
},
|
||||
/* 4. 浮空岛 */
|
||||
4(put, clr, ox, oy, oz, rnd) {
|
||||
const R = 9 + (rnd() * 4 | 0);
|
||||
for (let dx = -R; dx <= R; dx++) for (let dz = -R; dz <= R; dz++) for (let dy = -6; dy <= 3; dy++) {
|
||||
const rr = R * (1 - Math.abs(dy + 1.5) / 9) * (0.75 + 0.4 * fbm((ox + dx) * 0.15, (oz + dz) * 0.15, 2));
|
||||
if (dx * dx + dz * dz <= rr * rr) {
|
||||
let id = STONE;
|
||||
if (dy >= 2) id = GRASS; else if (dy >= 0) id = DIRT;
|
||||
put(ox + dx, oy + dy, oz + dz, id);
|
||||
}
|
||||
}
|
||||
// 垂挂魔晶
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const a = rnd() * Math.PI * 2, r = rnd() * (R - 4);
|
||||
const bx = ox + Math.round(Math.cos(a) * r), bz = oz + Math.round(Math.sin(a) * r);
|
||||
const len = 2 + (rnd() * 4 | 0);
|
||||
for (let y = 0; y < len; y++) put(bx, oy - 7 - y, bz, y === len - 1 ? RUNE : CRYSTAL);
|
||||
}
|
||||
// 岛上小树
|
||||
const th = 4;
|
||||
for (let y = 4; y < 4 + th; y++) put(ox, oy + y, oz, LOG);
|
||||
for (let dx = -2; dx <= 2; dx++) for (let dz = -2; dz <= 2; dz++) for (let dy = 0; dy <= 2; dy++)
|
||||
if (Math.abs(dx) + Math.abs(dz) + dy < 5) put(ox + dx, oy + 4 + th - 1 + dy, oz + dz, LEAVES, 1);
|
||||
put(ox + 3, oy + 4, oz + 2, GOLD, 1); put(ox - 3, oy + 4, oz - 1, CRYSTAL, 1);
|
||||
},
|
||||
/* 5. 巫师塔 */
|
||||
5(put, clr, ox, oy, oz, rnd) {
|
||||
const H = 16 + (rnd() * 5 | 0), R = 3.4;
|
||||
for (let y = 0; y < H; y++) for (let dx = -4; dx <= 4; dx++) for (let dz = -4; dz <= 4; dz++) {
|
||||
const d = Math.sqrt(dx * dx + dz * dz);
|
||||
if (d <= R && d >= R - 1.2) {
|
||||
let id = BRICK;
|
||||
if (y > 3 && y % 5 === 2 && (Math.abs(dx) === 3 || Math.abs(dz) === 3) && (Math.abs(dx) < 3 || Math.abs(dz) < 3)) id = CRYSTAL; // 魔晶窗
|
||||
put(ox + dx, oy + y, oz + dz, id);
|
||||
}
|
||||
if (y === 0 && d < R) put(ox + dx, oy - 1, oz + dz, STONE);
|
||||
}
|
||||
// 门
|
||||
clr(ox, oy, oz + 3); clr(ox, oy + 1, oz + 3);
|
||||
// 顶层瞭望台 + 尖顶
|
||||
for (let dx = -4; dx <= 4; dx++) for (let dz = -4; dz <= 4; dz++) { const d = Math.sqrt(dx * dx + dz * dz); if (d <= 4.2 && d >= 3.2) put(ox + dx, oy + H, oz + dz, BRICK); }
|
||||
for (let dy = 1; dy <= 6; dy++) { const r = 4 * (1 - dy / 7); for (let dx = -4; dx <= 4; dx++) for (let dz = -4; dz <= 4; dz++) if (Math.sqrt(dx * dx + dz * dz) <= r) put(ox + dx, oy + H + dy, oz + dz, OBSIDIAN); }
|
||||
put(ox, oy + H + 7, oz, RUNE);
|
||||
},
|
||||
/* 6. 巨灵古树 */
|
||||
6(put, clr, ox, oy, oz, rnd) {
|
||||
const H = 18 + (rnd() * 8 | 0);
|
||||
for (let y = 0; y < H; y++) for (let dx = 0; dx <= 2; dx++) for (let dz = 0; dz <= 2; dz++) put(ox + dx, oy + y, oz + dz, LOG);
|
||||
// 根
|
||||
for (const [rx0, rz0] of [[-2, 0], [4, 1], [1, -2], [0, 4], [-1, -1], [3, 3]])
|
||||
for (let y = 0; y < 3; y++) put(ox + rx0, oy + y, oz + rz0, LOG);
|
||||
// 巨冠
|
||||
for (let dx = -9; dx <= 11; dx++) for (let dz = -9; dz <= 11; dz++) for (let dy = -3; dy <= 4; dy++) {
|
||||
const d = Math.sqrt((dx - 1) * (dx - 1) / 1.4 + dy * dy * 2.2 + (dz - 1) * (dz - 1) / 1.4);
|
||||
if (d < 7 + fbm((ox + dx) * 0.3, (oz + dz) * 0.3, 2) * 2) put(ox + dx, oy + H + dy, oz + dz, LEAVES, 1);
|
||||
}
|
||||
// 垂藤与发光果
|
||||
for (let k = 0; k < 10; k++) {
|
||||
const a = rnd() * Math.PI * 2, r = 4 + rnd() * 5;
|
||||
const bx = ox + Math.round(Math.cos(a) * r), bz = oz + Math.round(Math.sin(a) * r);
|
||||
const len = 2 + (rnd() * 4 | 0);
|
||||
for (let y = 0; y < len; y++) put(bx, oy + H - 4 - y, bz, LEAVES, 1);
|
||||
if (rnd() < 0.5) put(bx, oy + H - 5 - len, bz, CRYSTAL, 1);
|
||||
}
|
||||
},
|
||||
/* 7. 黑曜石传送门 */
|
||||
7(put, clr, ox, oy, oz, rnd) {
|
||||
for (let dx = -3; dx <= 4; dx++) for (let dz = -2; dz <= 2; dz++) { put(ox + dx, oy, oz + dz, OBSIDIAN); for (let y = 1; y <= 5; y++) clr(ox + dx, oy + y, oz + dz); }
|
||||
for (let y = 1; y <= 5; y++) { put(ox, oy + y, oz, OBSIDIAN); put(ox + 3, oy + y, oz, OBSIDIAN); }
|
||||
for (let dx = 0; dx <= 3; dx++) { put(ox + dx, oy + 5, oz, OBSIDIAN); put(ox + dx, oy + 1, oz, OBSIDIAN); }
|
||||
for (let dx = 1; dx <= 2; dx++) for (let y = 2; y <= 4; y++) put(ox + dx, oy + y, oz, PORTAL);
|
||||
put(ox - 1, oy + 1, oz, GOLD); put(ox + 4, oy + 1, oz, GOLD);
|
||||
put(ox - 1, oy + 2, oz, RUNE); put(ox + 4, oy + 2, oz, RUNE);
|
||||
// 岩浆池
|
||||
for (let dx = -1; dx <= 1; dx++) for (let dz = -1; dz <= 1; dz++) put(ox + 6 + dx, oy, oz + 3 + dz, Math.abs(dx) === 1 || Math.abs(dz) === 1 ? STONE : LAVA);
|
||||
},
|
||||
/* 8. 天空残拱 */
|
||||
8(put, clr, ox, oy, oz, rnd) {
|
||||
const gap = 6, h1 = 6 + (rnd() * 3 | 0), h2 = 6 + (rnd() * 3 | 0);
|
||||
for (let y = 1; y <= h1; y++) put(ox, oy + y, oz, MARBLE);
|
||||
for (let y = 1; y <= h2; y++) put(ox + gap, oy + y, oz, MARBLE);
|
||||
const top = Math.max(h1, h2) + 1;
|
||||
for (let dx = 0; dx <= gap; dx++) { put(ox + dx, oy + top, oz, MARBLE); if (dx > 0 && dx < gap && rnd() < 0.6) put(ox + dx, oy + top + 1, oz, MARBLE); }
|
||||
put(ox, oy + h1 + 1 === oy + top ? oy + top + 1 : oy + h1 + 1, oz, RUNE, 1);
|
||||
// 散落碎石与苔藓
|
||||
for (let k = 0; k < 14; k++) {
|
||||
const bx = ox - 4 + (rnd() * 14 | 0), bz = oz - 4 + (rnd() * 9 | 0);
|
||||
const r = rnd();
|
||||
put(bx, oy + 1, bz, r < 0.4 ? MARBLE : r < 0.7 ? STONE : LEAVES, 1);
|
||||
}
|
||||
put(ox + 2, oy + 1, oz + 2, GOLD, 1);
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- 生成单个区块数据 ---------- */
|
||||
function generateChunkData(cx, cz) {
|
||||
const data = new Uint8Array(CHUNK * WORLD_H * CHUNK);
|
||||
const baseX = cx * CHUNK, baseZ = cz * CHUNK;
|
||||
// 供建筑/树木写入的工具(自动裁剪到本区块)
|
||||
const put = (wx, wy, wz, id, onlyAir = 0) => {
|
||||
if (wy < 1 || wy >= WORLD_H) return;
|
||||
const lx = wx - baseX, lz = wz - baseZ;
|
||||
if (lx < 0 || lx > 15 || lz < 0 || lz > 15) return;
|
||||
const i = cidx(lx, wy, lz);
|
||||
if (onlyAir && data[i] !== AIR) return;
|
||||
if (data[i] === BEDROCK) return;
|
||||
data[i] = id;
|
||||
};
|
||||
const clr = (wx, wy, wz) => put(wx, wy, wz, AIR);
|
||||
|
||||
// 1) 地形
|
||||
for (let lx = 0; lx < 16; lx++) for (let lz = 0; lz < 16; lz++) {
|
||||
const wx = baseX + lx, wz = baseZ + lz;
|
||||
const h = terrainHeight(wx, wz);
|
||||
for (let y = 0; y < h; y++) {
|
||||
let id;
|
||||
if (y === 0) id = BEDROCK;
|
||||
else if (y < h - 4) id = STONE;
|
||||
else if (y < h - 1) id = DIRT;
|
||||
else id = h <= SEA + 1 ? SAND : GRASS;
|
||||
data[cidx(lx, y, lz)] = id;
|
||||
}
|
||||
for (let y = h; y < SEA; y++) data[cidx(lx, y, lz)] = WATER; // 海水
|
||||
// 魔力生态域:地表晶簇 / 符文砾
|
||||
if (h > SEA + 1) {
|
||||
const mg = magicField(wx, wz), r = hash2(wx * 3 + 1, wz * 3 - 2);
|
||||
if (mg > 0.28 && r < 0.028) {
|
||||
put(wx, h, wz, CRYSTAL);
|
||||
if (r < 0.012) { put(wx + 1, h, wz, CRYSTAL); put(wx, h + 1, wz, CRYSTAL); }
|
||||
} else if (r < 0.006) put(wx, h, wz, RUNE);
|
||||
}
|
||||
}
|
||||
// 2) 树木(含边缘溢出,从邻列回写)
|
||||
for (let lx = -3; lx < 19; lx++) for (let lz = -3; lz < 19; lz++) {
|
||||
const wx = baseX + lx, wz = baseZ + lz;
|
||||
const h = terrainHeight(wx, wz);
|
||||
const th = treeAt(wx, wz, h);
|
||||
if (!th) continue;
|
||||
for (let y = 0; y < th; y++) put(wx, h + y, wz, LOG);
|
||||
put(wx, h - 1, wz, DIRT);
|
||||
for (let dx = -2; dx <= 2; dx++) for (let dz = -2; dz <= 2; dz++) for (let dy = -2; dy <= 1; dy++) {
|
||||
const r = dy < 0 ? 2 : 1;
|
||||
if (Math.abs(dx) <= r && Math.abs(dz) <= r && !(dx === 0 && dz === 0 && dy < 0))
|
||||
put(wx + dx, h + th + dy, wz + dz, LEAVES, 1);
|
||||
}
|
||||
put(wx, h + th + 2, wz, LEAVES, 1);
|
||||
}
|
||||
// 3) 神话建筑(覆盖周边区域,跨界回写)
|
||||
const r0x = Math.floor((baseX - 48) / REGION), r1x = Math.floor((baseX + 15 + 48) / REGION);
|
||||
const r0z = Math.floor((baseZ - 48) / REGION), r1z = Math.floor((baseZ + 15 + 48) / REGION);
|
||||
for (let rx = r0x; rx <= r1x; rx++) for (let rz = r0z; rz <= r1z; rz++) {
|
||||
const s = getStructure(rx, rz);
|
||||
if (!s) continue;
|
||||
let sseed = Math.floor(s.seed * 1e9) + 1;
|
||||
const rnd = () => (sseed = (sseed * 9301 + 49297) % 233280) / 233280;
|
||||
STRUCTS[s.type](put, clr, s.ox, s.oy, s.oz, rnd);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
4. 区块网格化(面剔除 + 每面明暗顶点色)
|
||||
============================================================ */
|
||||
const FACES = [
|
||||
{ dir: [-1, 0, 0], shade: 0.80, corners: [{ p: [0, 1, 0], uv: [0, 1] }, { p: [0, 0, 0], uv: [0, 0] }, { p: [0, 1, 1], uv: [1, 1] }, { p: [0, 0, 1], uv: [1, 0] }] },
|
||||
{ dir: [1, 0, 0], shade: 0.80, corners: [{ p: [1, 1, 1], uv: [0, 1] }, { p: [1, 0, 1], uv: [0, 0] }, { p: [1, 1, 0], uv: [1, 1] }, { p: [1, 0, 0], uv: [1, 0] }] },
|
||||
{ dir: [0, -1, 0], shade: 0.55, corners: [{ p: [1, 0, 1], uv: [1, 0] }, { p: [0, 0, 1], uv: [0, 0] }, { p: [1, 0, 0], uv: [1, 1] }, { p: [0, 0, 0], uv: [0, 1] }] },
|
||||
{ dir: [0, 1, 0], shade: 1.00, corners: [{ p: [0, 1, 1], uv: [1, 1] }, { p: [1, 1, 1], uv: [0, 1] }, { p: [0, 1, 0], uv: [1, 0] }, { p: [1, 1, 0], uv: [0, 0] }] },
|
||||
{ dir: [0, 0, -1], shade: 0.68, corners: [{ p: [1, 0, 0], uv: [1, 0] }, { p: [0, 0, 0], uv: [0, 0] }, { p: [1, 1, 0], uv: [1, 1] }, { p: [0, 1, 0], uv: [0, 1] }] },
|
||||
{ dir: [0, 0, 1], shade: 0.68, corners: [{ p: [0, 0, 1], uv: [1, 0] }, { p: [1, 0, 1], uv: [0, 0] }, { p: [0, 1, 1], uv: [1, 1] }, { p: [1, 1, 1], uv: [0, 1] }] },
|
||||
];
|
||||
const EMISSIVE_TEX = new Set([11, 12, 13, 15, 16]);
|
||||
|
||||
function buildChunkMesh(cx, cz, scene) {
|
||||
const chunk = chunks.get(ckey(cx, cz));
|
||||
if (!chunk || !chunk.data) return;
|
||||
if (chunk.mesh) { scene.remove(chunk.mesh); chunk.mesh.geometry.dispose(); chunk.mesh = null; }
|
||||
const buckets = new Map();
|
||||
const baseX = cx * CHUNK, baseZ = cz * CHUNK;
|
||||
for (let y = 0; y < WORLD_H; y++) for (let z = 0; z < 16; z++) for (let x = 0; x < 16; x++) {
|
||||
const id = chunk.data[cidx(x, y, z)];
|
||||
if (!id) continue;
|
||||
const wx = baseX + x, wz = baseZ + z;
|
||||
const texs = BLOCK_TEX[id];
|
||||
for (const f of FACES) {
|
||||
const nb = getBlock(wx + f.dir[0], y + f.dir[1], wz + f.dir[2]);
|
||||
if (id === WATER) { if (nb !== AIR) continue; }
|
||||
else if (nb !== AIR && nb !== WATER) continue;
|
||||
const texId = f.dir[1] === 1 ? texs[0] : f.dir[1] === -1 ? texs[2] : texs[1];
|
||||
let b = buckets.get(texId);
|
||||
if (!b) { b = { pos: [], norm: [], uv: [], col: [], idx: [] }; buckets.set(texId, b); }
|
||||
const bright = EMISSIVE_TEX.has(texId) ? 1 : f.shade;
|
||||
const ndx = b.pos.length / 3;
|
||||
for (const c of f.corners) {
|
||||
b.pos.push(wx + c.p[0], y + c.p[1], wz + c.p[2]);
|
||||
b.norm.push(f.dir[0], f.dir[1], f.dir[2]);
|
||||
b.uv.push(c.uv[0], c.uv[1]);
|
||||
b.col.push(bright, bright, bright);
|
||||
}
|
||||
b.idx.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3);
|
||||
}
|
||||
}
|
||||
if (buckets.size === 0) return;
|
||||
const pos = [], norm = [], uv = [], col = [], idx = [];
|
||||
const geo = new THREE.BufferGeometry();
|
||||
let start = 0;
|
||||
for (const [texId, b] of [...buckets.entries()].sort((a, b2) => a[0] - b2[0])) {
|
||||
const off = pos.length / 3;
|
||||
pos.push(...b.pos); norm.push(...b.norm); uv.push(...b.uv); col.push(...b.col);
|
||||
for (const i of b.idx) idx.push(i + off);
|
||||
geo.addGroup(start, b.idx.length, texId);
|
||||
start += b.idx.length;
|
||||
}
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
geo.setAttribute('normal', new THREE.Float32BufferAttribute(norm, 3));
|
||||
geo.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
|
||||
geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
|
||||
geo.setIndex(idx);
|
||||
const mesh = new THREE.Mesh(geo, MATERIALS);
|
||||
mesh.matrixAutoUpdate = false;
|
||||
scene.add(mesh);
|
||||
chunk.mesh = mesh;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
5. 场景 / 光照 / 天空
|
||||
============================================================ */
|
||||
const canvas = document.getElementById('game');
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
const scene = new THREE.Scene();
|
||||
scene.fog = new THREE.Fog(0xa8d0f0, 50, RENDER_DIST * 16 - 8);
|
||||
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
|
||||
camera.rotation.order = 'YXZ';
|
||||
|
||||
// 渐变天穹
|
||||
const sky = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(600, 24, 12),
|
||||
new THREE.ShaderMaterial({
|
||||
side: THREE.BackSide, fog: false, depthWrite: false,
|
||||
vertexShader: 'varying vec3 vP; void main(){ vP=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }',
|
||||
fragmentShader: 'varying vec3 vP; void main(){ float t=clamp(normalize(vP).y*1.4+0.25,0.0,1.0); vec3 c=mix(vec3(0.82,0.90,0.97),vec3(0.25,0.52,0.86),t); gl_FragColor=vec4(c,1.0); }'
|
||||
})
|
||||
);
|
||||
scene.add(sky);
|
||||
// 太阳
|
||||
const sunMesh = new THREE.Mesh(new THREE.CircleGeometry(28, 24), new THREE.MeshBasicMaterial({ color: 0xfff4c0, fog: false }));
|
||||
sunMesh.position.set(220, 320, 140); sunMesh.lookAt(0, 0, 0); scene.add(sunMesh);
|
||||
scene.add(new THREE.HemisphereLight(0xcfe4ff, 0x6b5b45, 0.95));
|
||||
const sun = new THREE.DirectionalLight(0xfff2d8, 1.05);
|
||||
sun.position.set(120, 200, 80); scene.add(sun);
|
||||
// 云
|
||||
const clouds = [];
|
||||
{
|
||||
const cm = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.75 });
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const w = 14 + Math.random() * 26, d = 10 + Math.random() * 18;
|
||||
const c = new THREE.Mesh(new THREE.BoxGeometry(w, 1.6, d), cm);
|
||||
c.position.set((Math.random() - 0.5) * 600, 100 + Math.random() * 14, (Math.random() - 0.5) * 600);
|
||||
scene.add(c); clouds.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
6. 玩家:第一人称物理(碰撞 / 飞行 / 游泳)
|
||||
============================================================ */
|
||||
const SPAWN = { x: 8, z: 8 };
|
||||
const player = {
|
||||
pos: new THREE.Vector3(SPAWN.x + 0.5, terrainHeight(SPAWN.x, SPAWN.z) + 2, SPAWN.z + 0.5),
|
||||
vel: new THREE.Vector3(),
|
||||
yaw: 0, pitch: 0, fly: false, onGround: false,
|
||||
};
|
||||
const keys = {};
|
||||
const PR = 0.3, PH = 1.8, EYE = 1.62, EPS = 0.001;
|
||||
|
||||
function collideAxis(axis, delta) {
|
||||
if (delta === 0) return;
|
||||
player.pos[axis] += delta;
|
||||
const p = player.pos;
|
||||
const minX = p.x - PR, maxX = p.x + PR, minY = p.y, maxY = p.y + PH, minZ = p.z - PR, maxZ = p.z + PR;
|
||||
for (let bx = Math.floor(minX); bx <= Math.floor(maxX - EPS); bx++)
|
||||
for (let by = Math.floor(minY); by <= Math.floor(maxY - EPS); by++)
|
||||
for (let bz = Math.floor(minZ); bz <= Math.floor(maxZ - EPS); bz++) {
|
||||
if (!isSolid(getBlock(bx, by, bz))) continue;
|
||||
if (axis === 'y') {
|
||||
if (delta < 0) { p.y = by + 1; player.vel.y = 0; player.onGround = true; }
|
||||
else { p.y = by - PH - EPS; player.vel.y = 0; }
|
||||
} else if (axis === 'x') {
|
||||
p.x = delta > 0 ? bx - PR - EPS : bx + 1 + PR + EPS;
|
||||
} else {
|
||||
p.z = delta > 0 ? bz - PR - EPS : bz + 1 + PR + EPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
function inWater() {
|
||||
const p = player.pos;
|
||||
return getBlock(Math.floor(p.x), Math.floor(p.y + 0.4), Math.floor(p.z)) === WATER ||
|
||||
getBlock(Math.floor(p.x), Math.floor(p.y + 1.2), Math.floor(p.z)) === WATER;
|
||||
}
|
||||
function updatePlayer(dt) {
|
||||
const speed = player.fly ? 13 : (keys.ShiftLeft ? 8.2 : 5.4);
|
||||
let mx = 0, mz = 0;
|
||||
if (keys.KeyW) mz += 1; if (keys.KeyS) mz -= 1;
|
||||
if (keys.KeyA) mx -= 1; if (keys.KeyD) mx += 1;
|
||||
const len = Math.hypot(mx, mz) || 1;
|
||||
const sin = Math.sin(player.yaw), cos = Math.cos(player.yaw);
|
||||
const wx = (mx * cos - mz * sin) / len * speed;
|
||||
const wz = (-mx * sin - mz * cos) / len * speed;
|
||||
const swim = inWater();
|
||||
if (player.fly) {
|
||||
player.vel.set(wx, 0, wz);
|
||||
if (keys.Space) player.vel.y = 10; else if (keys.ShiftLeft) player.vel.y = -10;
|
||||
} else {
|
||||
player.vel.x = wx; player.vel.z = wz;
|
||||
player.vel.y -= (swim ? 9 : 27) * dt;
|
||||
if (swim) player.vel.y = Math.max(player.vel.y, -3.2);
|
||||
if (keys.Space) {
|
||||
if (player.onGround) player.vel.y = 9.2;
|
||||
else if (swim) player.vel.y = 4.2;
|
||||
}
|
||||
}
|
||||
player.onGround = false;
|
||||
collideAxis('x', player.vel.x * dt);
|
||||
collideAxis('z', player.vel.z * dt);
|
||||
collideAxis('y', player.vel.y * dt);
|
||||
if (player.pos.y < -20) { player.pos.set(SPAWN.x + 0.5, terrainHeight(SPAWN.x, SPAWN.z) + 2, SPAWN.z + 0.5); player.vel.set(0, 0, 0); }
|
||||
camera.position.set(player.pos.x, player.pos.y + EYE, player.pos.z);
|
||||
camera.rotation.set(player.pitch, player.yaw, 0);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
7. 区块调度:按需加载 / 卸载 / 重建
|
||||
============================================================ */
|
||||
let loadQueue = [], dirtySet = new Set(), lastCX = 1e9, lastCZ = 1e9;
|
||||
function ensureChunks() {
|
||||
const pcx = Math.floor(player.pos.x / CHUNK), pcz = Math.floor(player.pos.z / CHUNK);
|
||||
if (pcx === lastCX && pcz === lastCZ) return;
|
||||
lastCX = pcx; lastCZ = pcz;
|
||||
loadQueue = [];
|
||||
for (let dx = -RENDER_DIST; dx <= RENDER_DIST; dx++) for (let dz = -RENDER_DIST; dz <= RENDER_DIST; dz++) {
|
||||
const cx = pcx + dx, cz = pcz + dz;
|
||||
if (!chunks.has(ckey(cx, cz))) loadQueue.push([cx, cz, dx * dx + dz * dz]);
|
||||
}
|
||||
loadQueue.sort((a, b) => a[2] - b[2]);
|
||||
for (const [k, c] of chunks) {
|
||||
const [cx, cz] = k.split(',').map(Number);
|
||||
if (Math.max(Math.abs(cx - pcx), Math.abs(cz - pcz)) > RENDER_DIST + 1) {
|
||||
if (c.mesh) { scene.remove(c.mesh); c.mesh.geometry.dispose(); }
|
||||
chunks.delete(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
function processQueues(budget) {
|
||||
let n = 0;
|
||||
while (loadQueue.length && n < budget) {
|
||||
const [cx, cz] = loadQueue.shift();
|
||||
if (chunks.has(ckey(cx, cz))) continue;
|
||||
chunks.set(ckey(cx, cz), { data: generateChunkData(cx, cz), mesh: null });
|
||||
n++;
|
||||
}
|
||||
for (const k of [...chunks.keys()]) {
|
||||
const c = chunks.get(k);
|
||||
if (!c.mesh && c.data) {
|
||||
const [cx, cz] = k.split(',').map(Number);
|
||||
buildChunkMesh(cx, cz, scene);
|
||||
// 邻居重建以修正边界剔除面
|
||||
for (const [dx, dz] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
const nc = chunks.get(ckey(cx + dx, cz + dz));
|
||||
if (nc && nc.mesh) dirtySet.add(ckey(cx + dx, cz + dz));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dirtySet.size) {
|
||||
const k = dirtySet.values().next().value;
|
||||
dirtySet.delete(k);
|
||||
const [cx, cz] = k.split(',').map(Number);
|
||||
if (chunks.has(k)) buildChunkMesh(cx, cz, scene);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
8. 方块破坏 / 放置(体素 DDA 射线)
|
||||
============================================================ */
|
||||
function raycastVoxel(maxDist) {
|
||||
const o = camera.position, d = new THREE.Vector3();
|
||||
camera.getWorldDirection(d);
|
||||
let x = Math.floor(o.x), y = Math.floor(o.y), z = Math.floor(o.z);
|
||||
const sx = Math.sign(d.x) || 1, sy = Math.sign(d.y) || 1, sz = Math.sign(d.z) || 1;
|
||||
const tdx = Math.abs(1 / (d.x || 1e-9)), tdy = Math.abs(1 / (d.y || 1e-9)), tdz = Math.abs(1 / (d.z || 1e-9));
|
||||
let tmx = (d.x > 0 ? (x + 1 - o.x) : (o.x - x)) * tdx;
|
||||
let tmy = (d.y > 0 ? (y + 1 - o.y) : (o.y - y)) * tdy;
|
||||
let tmz = (d.z > 0 ? (z + 1 - o.z) : (o.z - z)) * tdz;
|
||||
let face = null, t = 0;
|
||||
for (let i = 0; i < 128; i++) {
|
||||
const id = getBlock(x, y, z);
|
||||
if (id !== AIR && id !== WATER) return { x, y, z, face, id };
|
||||
if (tmx < tmy && tmx < tmz) { x += sx; t = tmx; tmx += tdx; face = [-sx, 0, 0]; }
|
||||
else if (tmy < tmz) { y += sy; t = tmy; tmy += tdy; face = [0, -sy, 0]; }
|
||||
else { z += sz; t = tmz; tmz += tdz; face = [0, 0, -sz]; }
|
||||
if (t > maxDist) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function setBlockAndRemesh(wx, wy, wz, id) {
|
||||
if (wy < 1 || wy >= WORLD_H) return;
|
||||
const cx = Math.floor(wx / CHUNK), cz = Math.floor(wz / CHUNK);
|
||||
const c = chunks.get(ckey(cx, cz));
|
||||
if (!c) return;
|
||||
const lx = wx - cx * CHUNK, lz = wz - cz * CHUNK;
|
||||
if (c.data[cidx(lx, wy, lz)] === BEDROCK) return;
|
||||
c.data[cidx(lx, wy, lz)] = id;
|
||||
buildChunkMesh(cx, cz, scene);
|
||||
if (lx === 0) dirtySet.add(ckey(cx - 1, cz));
|
||||
if (lx === 15) dirtySet.add(ckey(cx + 1, cz));
|
||||
if (lz === 0) dirtySet.add(ckey(cx, cz - 1));
|
||||
if (lz === 15) dirtySet.add(ckey(cx, cz + 1));
|
||||
}
|
||||
function breakBlock() {
|
||||
const hit = raycastVoxel(7);
|
||||
if (hit) setBlockAndRemesh(hit.x, hit.y, hit.z, AIR);
|
||||
}
|
||||
function placeBlock() {
|
||||
const hit = raycastVoxel(7);
|
||||
if (!hit || !hit.face) return;
|
||||
const tx = hit.x + hit.face[0], ty = hit.y + hit.face[1], tz = hit.z + hit.face[2];
|
||||
const cur = getBlock(tx, ty, tz);
|
||||
if (cur !== AIR && cur !== WATER) return;
|
||||
// 不与玩家重叠
|
||||
const p = player.pos;
|
||||
if (tx + 1 > p.x - PR && tx < p.x + PR && ty + 1 > p.y && ty < p.y + PH && tz + 1 > p.z - PR && tz < p.z + PR) return;
|
||||
setBlockAndRemesh(tx, ty, tz, HOTBAR[sel]);
|
||||
}
|
||||
// 选中方块高亮框
|
||||
const hl = new THREE.LineSegments(
|
||||
new THREE.EdgesGeometry(new THREE.BoxGeometry(1.002, 1.002, 1.002)),
|
||||
new THREE.LineBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.7 })
|
||||
);
|
||||
hl.visible = false; scene.add(hl);
|
||||
|
||||
/* ============================================================
|
||||
9. 快捷栏 / 输入
|
||||
============================================================ */
|
||||
const HOTBAR = [GRASS, DIRT, STONE, LOG, PLANKS, SAND, MARBLE, CRYSTAL, OBSIDIAN];
|
||||
let sel = 0;
|
||||
const hotbarEl = document.getElementById('hotbar');
|
||||
HOTBAR.forEach((id, i) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'slot' + (i === 0 ? ' sel' : '');
|
||||
const img = document.createElement('img');
|
||||
img.src = texCanvases[BLOCK_TEX[id][1]].toDataURL();
|
||||
d.appendChild(img);
|
||||
const n = document.createElement('span'); n.className = 'num'; n.textContent = i + 1; d.appendChild(n);
|
||||
const nm = document.createElement('span'); nm.className = 'name'; nm.textContent = BLOCK_NAMES[id]; d.appendChild(nm);
|
||||
hotbarEl.appendChild(d);
|
||||
});
|
||||
function selectSlot(i) {
|
||||
sel = (i + HOTBAR.length) % HOTBAR.length;
|
||||
[...hotbarEl.children].forEach((el, k) => el.classList.toggle('sel', k === sel));
|
||||
}
|
||||
|
||||
const overlay = document.getElementById('overlay');
|
||||
if (location.search.includes('noui')) overlay.classList.add('hidden');
|
||||
let locked = false, lmb = false, rmb = false, lastAct = 0;
|
||||
canvas.addEventListener('click', () => { if (!locked) canvas.requestPointerLock(); });
|
||||
overlay.addEventListener('click', () => canvas.requestPointerLock());
|
||||
document.addEventListener('pointerlockchange', () => {
|
||||
locked = document.pointerLockElement === canvas;
|
||||
overlay.classList.toggle('hidden', locked);
|
||||
});
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (!locked) return;
|
||||
player.yaw -= e.movementX * 0.0024;
|
||||
player.pitch -= e.movementY * 0.0024;
|
||||
player.pitch = Math.max(-1.55, Math.min(1.55, player.pitch));
|
||||
});
|
||||
document.addEventListener('mousedown', e => { if (!locked) return; if (e.button === 0) lmb = true; if (e.button === 2) rmb = true; });
|
||||
document.addEventListener('mouseup', e => { if (e.button === 0) lmb = false; if (e.button === 2) rmb = false; });
|
||||
document.addEventListener('contextmenu', e => e.preventDefault());
|
||||
document.addEventListener('wheel', e => { if (locked) selectSlot(sel + (e.deltaY > 0 ? 1 : -1)); });
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code.startsWith('Digit')) { const n = +e.code.slice(5); if (n >= 1 && n <= 9) selectSlot(n - 1); }
|
||||
if (e.code === 'KeyF') player.fly = !player.fly;
|
||||
if (e.code === 'Space') e.preventDefault();
|
||||
});
|
||||
document.addEventListener('keyup', e => keys[e.code] = false);
|
||||
addEventListener('resize', () => {
|
||||
camera.aspect = innerWidth / innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
10. 初始化与主循环
|
||||
============================================================ */
|
||||
// 预生成出生点周边
|
||||
for (let dx = -2; dx <= 2; dx++) for (let dz = -2; dz <= 2; dz++) {
|
||||
const cx = dx, cz = dz;
|
||||
chunks.set(ckey(cx, cz), { data: generateChunkData(cx, cz), mesh: null });
|
||||
}
|
||||
for (let dx = -2; dx <= 2; dx++) for (let dz = -2; dz <= 2; dz++) buildChunkMesh(dx, dz, scene);
|
||||
document.getElementById('loading').remove();
|
||||
|
||||
const statsEl = document.getElementById('stats');
|
||||
let lastT = performance.now(), fps = 60, frames = 0, fpsT = 0;
|
||||
function loop() {
|
||||
requestAnimationFrame(loop);
|
||||
const now = performance.now();
|
||||
const dt = Math.min(0.05, (now - lastT) / 1000);
|
||||
lastT = now;
|
||||
frames++; fpsT += dt;
|
||||
if (fpsT >= 0.5) { fps = Math.round(frames / fpsT); frames = 0; fpsT = 0; }
|
||||
|
||||
ensureChunks();
|
||||
processQueues(2);
|
||||
if (locked) updatePlayer(dt);
|
||||
else { camera.position.set(player.pos.x, player.pos.y + EYE, player.pos.z); camera.rotation.set(player.pitch, player.yaw, 0); }
|
||||
|
||||
// 持续破坏/放置(按住)
|
||||
if (locked && (lmb || rmb) && now - lastAct > 220) {
|
||||
if (lmb) breakBlock(); else placeBlock();
|
||||
lastAct = now;
|
||||
}
|
||||
// 高亮目标方块
|
||||
const hit = locked ? raycastVoxel(7) : null;
|
||||
if (hit) { hl.visible = true; hl.position.set(hit.x + 0.5, hit.y + 0.5, hit.z + 0.5); }
|
||||
else hl.visible = false;
|
||||
|
||||
// 天空与云跟随
|
||||
sky.position.copy(camera.position);
|
||||
for (const c of clouds) {
|
||||
c.position.x += dt * 1.6;
|
||||
if (c.position.x - camera.position.x > 320) c.position.x -= 640;
|
||||
if (c.position.x - camera.position.x < -320) c.position.x += 640;
|
||||
if (Math.abs(c.position.z - camera.position.z) > 320) c.position.z = camera.position.z + (Math.random() - 0.5) * 600;
|
||||
}
|
||||
sunMesh.position.set(camera.position.x + 220, 320, camera.position.z + 140);
|
||||
sunMesh.lookAt(camera.position);
|
||||
|
||||
statsEl.textContent = `FPS ${fps} 区块 ${chunks.size}\n坐标 ${player.pos.x.toFixed(1)}, ${player.pos.y.toFixed(1)}, ${player.pos.z.toFixed(1)}${player.fly ? '\n🕊 飞行模式' : ''}`;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
loop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user