Files
nagi-bench/outputs/kimi-k3/kimi-code-max/mythos-craft.html
T
Jesse ZhangandClaude Opus 4.8 63570b1e6e Add Kimi K3 (Kimi Code, Max) — mythos, pelican, turf-war 蚕食
Turf-war agent registered (policy stays private on the referee side).
Diagnosis: legal + fuel-safe but weak, loses ~3:1 to baseline (318:1045).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 01:22:54 +01:00

1190 lines
47 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>神话境界 MythosCraft</title>
<style>
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#0b0f16;
font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;user-select:none;-webkit-user-select:none;}
#game{position:fixed;inset:0;}
#game canvas{display:block;}
#vignette{position:fixed;inset:0;pointer-events:none;z-index:5;
background:radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.35) 100%);}
#crosshair{position:fixed;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;z-index:6;
pointer-events:none;mix-blend-mode:difference;}
#crosshair::before,#crosshair::after{content:"";position:absolute;background:#fff;}
#crosshair::before{left:10px;top:0;width:2px;height:22px;}
#crosshair::after{left:0;top:10px;width:22px;height:2px;}
#hotbar{position:fixed;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:4px;z-index:7;
padding:4px;background:rgba(0,0,0,0.35);border:2px solid rgba(0,0,0,0.55);border-radius:6px;pointer-events:none;}
.slot{width:46px;height:46px;position:relative;border:2px solid rgba(255,255,255,0.25);border-radius:3px;
background:rgba(20,20,20,0.5);}
.slot canvas{width:38px;height:38px;margin:2px;image-rendering:pixelated;}
.slot .k{position:absolute;left:3px;top:1px;font-size:9px;color:#ddd;text-shadow:1px 1px 0 #000;z-index:2;}
.slot.sel{border-color:#fff;box-shadow:0 0 0 2px rgba(255,255,255,0.35);}
#itemName{position:fixed;left:50%;bottom:78px;transform:translateX(-50%);z-index:7;color:#fff;font-size:15px;
text-shadow:1px 1px 2px #000;opacity:0;transition:opacity .4s;pointer-events:none;}
#debug{position:fixed;left:10px;top:10px;z-index:7;color:#fff;font:12px/1.5 ui-monospace,Menlo,monospace;
text-shadow:1px 1px 1px #000;white-space:pre;pointer-events:none;}
#hint{position:fixed;right:10px;bottom:10px;z-index:7;color:rgba(255,255,255,0.85);font-size:11px;
text-shadow:1px 1px 1px #000;text-align:right;line-height:1.6;pointer-events:none;}
#overlay{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;
background:linear-gradient(180deg, rgba(8,10,18,0.74), rgba(8,10,18,0.55) 40%, rgba(8,10,18,0.80));cursor:pointer;}
#overlay h1{margin:0;font-size:52px;letter-spacing:12px;color:#f3e9c8;
text-shadow:0 3px 0 #5a4a1f,0 6px 18px rgba(0,0,0,0.7);font-family:Georgia,"STKaiti","KaiTi",serif;}
#overlay h2{margin:8px 0 26px;font-size:14px;letter-spacing:6px;color:#c9b982;font-weight:normal;}
#overlay .card{background:rgba(0,0,0,0.42);border:1px solid rgba(240,220,160,0.25);border-radius:8px;
padding:14px 26px;color:#e8e2d0;font-size:13px;line-height:2;}
#overlay .card b{color:#ffd876;display:inline-block;min-width:96px;}
#overlay #startTip{margin-top:26px;font-size:18px;color:#fff;animation:pulse 1.6s infinite;}
@keyframes pulse{0%,100%{opacity:0.45}50%{opacity:1}}
#overlay.hidden{display:none;}
</style>
</head>
<body>
<div id="game"></div>
<div id="vignette"></div>
<div id="crosshair"></div>
<div id="hotbar"></div>
<div id="itemName"></div>
<div id="debug"></div>
<div id="hint">WASD 移动 · 空格 跳跃 · 左键 破坏 · 右键 放置<br>F 飞行 · Shift 疾跑/下降 · 1-0/滚轮 选方块 · R 回出生点</div>
<div id="overlay">
<h1>神话境界</h1>
<h2>MYTHOSCRAFT · 方块构筑的神话世界</h2>
<div class="card">
<div><b>WASD / 鼠标</b>移动与环视(点击进入指针锁定)</div>
<div><b>左键 / 右键</b>破坏方块 / 放置方块(按住可连续)</div>
<div><b>1-0 / 滚轮</b>切换手中的方块</div>
<div><b>空格 / Shift</b>跳跃 · 疾跑(飞行时上升/下降)</div>
<div><b>F / R</b>切换飞行 · 返回出生点</div>
<div><b>探索目标</b>天空神殿 · 浮空圣岛 · 符文石阵 · 世界之树 · 巨龙之塔 · 远古之门 · 荒野遗迹</div>
</div>
<div id="startTip">正在生成世界……</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
'use strict';
/* ============================================================
* 神话境界 MythosCraft —— 单文件体素沙盒
* 地形: Perlin fBm 无限区块生成
* 渲染: Three.js + 程序化 16x16 像素贴图集 + 面剔除网格化
* ============================================================ */
/* ---------------- 随机数与噪声 ---------------- */
function mulberry32(a){
return function(){
a|=0; a=a+0x6D2B79F5|0;
let t=Math.imul(a^a>>>15,1|a);
t=t+Math.imul(t^t>>>7,61|t)^t;
return((t^t>>>14)>>>0)/4294967296;
};
}
function hash2(x,z,s){
let h=(Math.imul(x,374761393)+Math.imul(z,668265263)+Math.imul(s,974634))|0;
h=Math.imul(h^h>>>13,1274126177);
return((h^h>>>16)>>>0)/4294967296;
}
const PERM=new Uint8Array(512);
(function(){
const p=new Uint8Array(256);
for(let i=0;i<256;i++)p[i]=i;
const r=mulberry32(20260601);
for(let i=255;i>0;i--){const j=(r()*(i+1))|0;const t=p[i];p[i]=p[j];p[j]=t;}
for(let i=0;i<512;i++)PERM[i]=p[i&255];
})();
function fade(t){return t*t*t*(t*(t*6-15)+10);}
function grad2(h,x,y){switch(h&3){case 0:return x+y;case 1:return x-y;case 2:return -x+y;default:return -x-y;}}
function perlin2(x,y){
const X=Math.floor(x),Y=Math.floor(y);
const xi=X&255,yi=Y&255;
x-=X;y-=Y;
const u=fade(x),v=fade(y);
const aa=PERM[PERM[xi]+yi],ab=PERM[PERM[xi]+yi+1];
const ba=PERM[PERM[xi+1]+yi],bb=PERM[PERM[xi+1]+yi+1];
const x1=x-1,y1=y-1;
const l1=grad2(aa,x,y)+(grad2(ba,x1,y)-grad2(aa,x,y))*u;
const l2=grad2(ab,x,y1)+(grad2(bb,x1,y1)-grad2(ab,x,y1))*u;
return l1+(l2-l1)*v;
}
function fbm(x,y,oct){
let amp=0.5,f=1,sum=0,norm=0;
for(let i=0;i<oct;i++){sum+=amp*perlin2(x*f,y*f);norm+=amp;amp*=0.5;f*=2.03;}
return sum/norm;
}
/* ---------------- 常量与方块定义 ---------------- */
const CHUNK=16, HEIGHT=112, SEA=24, RD=6;
const AIR=0,GRASS=1,DIRT=2,STONE=3,SAND=4,LOG=5,LEAVES=6,PLANKS=7,COBBLE=8,SBRICK=9,
GLOW=10,OBSIDIAN=11,BEDROCK=12,WATER=13,GOLD=14,MARBLE=15,CRYSTAL=16,MOSSY=17,SNOW=18;
// 贴图集格子索引
const T={grassTop:0,grassSide:1,dirt:2,stone:3,sand:4,logSide:5,logTop:6,leaves:7,planks:8,cobble:9,
sbrick:10,glow:11,obsidian:12,bedrock:13,water:14,gold:15,marble:16,crystal:17,mossy:18,snow:19};
const BLOCKS=[];
BLOCKS[GRASS] ={name:'草方块', top:T.grassTop, side:T.grassSide, bottom:T.dirt};
BLOCKS[DIRT] ={name:'泥土', top:T.dirt, side:T.dirt, bottom:T.dirt};
BLOCKS[STONE] ={name:'石头', top:T.stone, side:T.stone, bottom:T.stone};
BLOCKS[SAND] ={name:'沙子', top:T.sand, side:T.sand, bottom:T.sand};
BLOCKS[LOG] ={name:'原木', top:T.logTop, side:T.logSide, bottom:T.logTop};
BLOCKS[LEAVES] ={name:'树叶', top:T.leaves, side:T.leaves, bottom:T.leaves};
BLOCKS[PLANKS] ={name:'木板', top:T.planks, side:T.planks, bottom:T.planks};
BLOCKS[COBBLE] ={name:'圆石', top:T.cobble, side:T.cobble, bottom:T.cobble};
BLOCKS[SBRICK] ={name:'石砖', top:T.sbrick, side:T.sbrick, bottom:T.sbrick};
BLOCKS[GLOW] ={name:'荧石', top:T.glow, side:T.glow, bottom:T.glow};
BLOCKS[OBSIDIAN]={name:'黑曜石', top:T.obsidian, side:T.obsidian, bottom:T.obsidian};
BLOCKS[BEDROCK] ={name:'基岩', top:T.bedrock, side:T.bedrock, bottom:T.bedrock};
BLOCKS[WATER] ={name:'水', top:T.water, side:T.water, bottom:T.water};
BLOCKS[GOLD] ={name:'金块', top:T.gold, side:T.gold, bottom:T.gold};
BLOCKS[MARBLE] ={name:'大理石', top:T.marble, side:T.marble, bottom:T.marble};
BLOCKS[CRYSTAL] ={name:'紫水晶', top:T.crystal, side:T.crystal, bottom:T.crystal};
BLOCKS[MOSSY] ={name:'苔石', top:T.mossy, side:T.mossy, bottom:T.mossy};
BLOCKS[SNOW] ={name:'雪块', top:T.snow, side:T.snow, bottom:T.snow};
function isSolid(id){return id!==AIR&&id!==WATER;}
/* ---------------- 程序化贴图集(16x16 像素风) ---------------- */
const ATLAS_COLS=8, ATLAS_ROWS=4, TILE=16;
const atlasCanvas=document.createElement('canvas');
atlasCanvas.width=ATLAS_COLS*TILE; atlasCanvas.height=ATLAS_ROWS*TILE;
(function paintAtlas(){
const g=atlasCanvas.getContext('2d');
const R=mulberry32(918273);
function px(i,x,y,r,gg,b,a){
const c=i%ATLAS_COLS, ro=(i/ATLAS_COLS)|0;
g.fillStyle='rgba('+(r|0)+','+(gg|0)+','+(b|0)+','+(a===undefined?1:a)+')';
g.fillRect(c*TILE+x, ro*TILE+y, 1, 1);
}
function noise(i,br,bg,bb,vr,vg,vb){
for(let y=0;y<16;y++)for(let x=0;x<16;x++)
px(i,x,y, br+(R()*2-1)*vr, bg+(R()*2-1)*vg, bb+(R()*2-1)*vb);
}
// 0 草顶
noise(T.grassTop, 96,158,58, 16,20,10);
for(let k=0;k<26;k++)px(T.grassTop,(R()*16)|0,(R()*16)|0, 70+R()*20,118+R()*24,40+R()*12);
for(let k=0;k<14;k++)px(T.grassTop,(R()*16)|0,(R()*16)|0, 122+R()*20,186+R()*16,74+R()*14);
// 2 泥土
noise(T.dirt, 134,96,67, 20,15,11);
for(let k=0;k<12;k++)px(T.dirt,(R()*16)|0,(R()*16)|0, 104+R()*14,72+R()*10,48+R()*9);
for(let k=0;k<6;k++)px(T.dirt,(R()*16)|0,(R()*16)|0, 160+R()*16,124+R()*12,92+R()*10);
// 1 草皮侧面 = 泥土底 + 顶部草沿
noise(T.grassSide, 134,96,67, 20,15,11);
for(let x=0;x<16;x++){
const depth=2+((R()*3)|0);
for(let y=0;y<depth;y++)px(T.grassSide,x,y, 90+R()*22,150+R()*26,52+R()*14);
if(R()<0.5)px(T.grassSide,x,depth, 96+R()*20,152+R()*22,56+R()*12);
}
// 3 石头
noise(T.stone, 126,126,128, 11,11,12);
for(let k=0;k<10;k++)px(T.stone,(R()*16)|0,(R()*16)|0, 104+R()*10,104+R()*10,106+R()*10);
// 4 沙子
noise(T.sand, 219,205,158, 13,12,10);
for(let k=0;k<8;k++)px(T.sand,(R()*16)|0,(R()*16)|0, 188+R()*12,172+R()*12,128+R()*10);
// 5 原木侧面(竖纹树皮)
for(let x=0;x<16;x++){
const band=(x%4===0)?0.72:((x%4===2)?1.06:0.92);
for(let y=0;y<16;y++){
const n=(R()*2-1)*10;
px(T.logSide,x,y, (104+n)*band,(82+n)*band,(50+n)*band);
}
}
for(let k=0;k<3;k++){const x=(R()*16)|0,y=(R()*16)|0;px(T.logSide,x,y,70,54,32);px(T.logSide,x,y+1,70,54,32);}
// 6 原木顶面(年轮)
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const dx=x-7.5,dy=y-7.5,d=Math.sqrt(dx*dx+dy*dy);
if(d>7.2){px(T.logTop,x,y, 96+R()*14,76+R()*12,46+R()*10);}
else{const ring=((d|0)%2===0)?1.0:0.78;const n=(R()*2-1)*8;
px(T.logTop,x,y,(176+n)*ring,(142+n)*ring,(96+n)*ring);}
}
// 7 树叶(带镂空孔洞)
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
if(R()<0.14){px(T.leaves,x,y,0,0,0,0);continue;}
const d=(R()<0.25)?0.6:1.0;
px(T.leaves,x,y,(46+R()*22)*d,(112+R()*30)*d,(34+R()*16)*d);
}
// 8 木板
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const board=(y>>2), seam=(y%4===3), end=((x+board*5)%16)<1;
if(seam){px(T.planks,x,y,122,96,58);continue;}
if(end){px(T.planks,x,y,130,102,62);continue;}
const n=(R()*2-1)*9, tone=1+(board%2?0.05:-0.04);
px(T.planks,x,y,(168+n)*tone,(132+n)*tone,(82+n)*tone);
}
// 9 圆石
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const cx=x>>2, cy=y>>2;
if(x%4===0||y%4===0){px(T.cobble,x,y,78+R()*8,78+R()*8,80+R()*8);continue;}
const v=hash2(cx,cy,7)*40-20;
px(T.cobble,x,y, 118+v+(R()*2-1)*10, 118+v+(R()*2-1)*10, 120+v+(R()*2-1)*10);
}
// 10 石砖
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const row=(y>>2), off=(row%2)?4:0;
if(y%4===0||((x+off)%8===0)){px(T.sbrick,x,y,92+R()*8,92+R()*8,94+R()*8);continue;}
px(T.sbrick,x,y, 128+(R()*2-1)*9,128+(R()*2-1)*9,130+(R()*2-1)*9);
}
// 11 荧石
noise(T.glow, 244,196,92, 22,20,12);
for(let k=0;k<16;k++)px(T.glow,(R()*16)|0,(R()*16)|0, 255,232,150+R()*60);
for(let k=0;k<9;k++)px(T.glow,(R()*16)|0,(R()*16)|0, 214+R()*20,140+R()*20,52+R()*16);
// 12 黑曜石
noise(T.obsidian, 20,16,30, 7,6,9);
for(let k=0;k<12;k++)px(T.obsidian,(R()*16)|0,(R()*16)|0, 46+R()*16,32+R()*12,72+R()*20);
// 13 基岩
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const v=(hash2(x,y,13)<0.5)?(52+R()*22):(108+R()*34);
px(T.bedrock,x,y,v,v,v+2);
}
// 14 水
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const streak=(y%5===0)?1.16:1.0;
px(T.water,x,y, (46+(R()*2-1)*8)*streak,(104+(R()*2-1)*10)*streak,(196+(R()*2-1)*14));
}
// 15 金块
noise(T.gold, 249,211,71, 14,12,8);
for(let k=0;k<10;k++)px(T.gold,(R()*16)|0,(R()*16)|0, 255,236,140+R()*40);
for(let y=0;y<16;y++){px(T.gold,0,y,196,158,44);px(T.gold,15,y,196,158,44);}
// 16 大理石
noise(T.marble, 233,231,223, 6,6,6);
for(let k=0;k<7;k++){let x=(R()*16)|0,y=(R()*16)|0;for(let s=0;s<4;s++){px(T.marble,Math.min(15,x+s),Math.min(15,y+((s*0.7)|0)),206,204,196);}}
// 17 紫水晶
noise(T.crystal, 168,92,222, 22,16,22);
for(let k=0;k<12;k++)px(T.crystal,(R()*16)|0,(R()*16)|0, 236,200,255);
for(let k=0;k<8;k++)px(T.crystal,(R()*16)|0,(R()*16)|0, 120,52,178);
// 18 苔石
for(let y=0;y<16;y++)for(let x=0;x<16;x++){
const cx=x>>2, cy=y>>2;
if(x%4===0||y%4===0){px(T.mossy,x,y,74+R()*8,80+R()*8,72+R()*8);continue;}
const moss=hash2(cx,cy,18)<0.45;
if(moss)px(T.mossy,x,y, 84+R()*16,124+R()*20,58+R()*12);
else{const v=hash2(cx,cy,7)*40-20;px(T.mossy,x,y, 116+v,116+v,118+v);}
}
// 19 雪
noise(T.snow, 240,244,247, 6,6,5);
for(let k=0;k<8;k++)px(T.snow,(R()*16)|0,(R()*16)|0, 218,226,236);
})();
function tileUV(i){
const c=i%ATLAS_COLS, r=(i/ATLAS_COLS)|0, e=0.55;
const W=ATLAS_COLS*TILE, H=ATLAS_ROWS*TILE;
const u0=(c*TILE+e)/W, u1=((c+1)*TILE-e)/W;
const v0=1-((r+1)*TILE-e)/H, v1=1-(r*TILE+e)/H;
return [u0,v0,u1,v1];
}
/* ---------------- 地形高度与湿度 ---------------- */
function heightAt(x,z){
const n1=fbm(x*0.0055, z*0.0055, 4); // 起伏丘陵
const n2=fbm(x*0.0016+131.7, z*0.0016-57.3, 3); // 山脉掩码
let h=30+n1*11;
const m=n2-0.18;
if(m>0)h+=m*m*90; // 山地抬升
return Math.max(6, Math.min(66, h));
}
function moistAt(x,z){return fbm(x*0.0035+400.2, z*0.0035-220.8, 2);}
/* ---------------- 世界存储 ---------------- */
const chunks=new Map(); // "cx,cz" -> {cx,cz,data,mesh,wmesh,built}
const edits=new Map(); // "x,y,z" -> id(玩家改动,重生成时回放)
function ckey(cx,cz){return cx+','+cz;}
function IDX(x,y,z){return (y*CHUNK+z)*CHUNK+x;}
function getBlock(wx,wy,wz){
if(wy<0)return BEDROCK;
if(wy>=HEIGHT)return AIR;
const c=chunks.get(ckey(Math.floor(wx/CHUNK),Math.floor(wz/CHUNK)));
if(!c)return AIR;
return c.data[IDX(wx-c.cx*CHUNK, wy, wz-c.cz*CHUNK)];
}
/* ---------------- 区块生成:地形 -> 树 -> 建筑 -> 玩家改动 ---------------- */
function genChunk(cx,cz){
const k=ckey(cx,cz);
let c=chunks.get(k);
if(c)return c;
const data=new Uint8Array(CHUNK*HEIGHT*CHUNK);
for(let lz=0;lz<CHUNK;lz++)for(let lx=0;lx<CHUNK;lx++){
const wx=cx*CHUNK+lx, wz=cz*CHUNK+lz;
const h=Math.max(2, Math.min(HEIGHT-30, Math.floor(heightAt(wx,wz))));
const topY=h-1;
for(let y=0;y<h;y++){
let id;
if(y===0)id=BEDROCK;
else if(y<h-4)id=STONE;
else if(y<topY)id=DIRT;
else id=(topY>=47)?SNOW:((topY<=SEA+1)?SAND:GRASS);
data[IDX(lx,y,lz)]=id;
}
if(topY<SEA)for(let y=h;y<SEA;y++)data[IDX(lx,y,lz)]=WATER;
}
c={cx:cx, cz:cz, data:data, mesh:null, wmesh:null, built:false};
chunks.set(k,c);
growTrees(c);
applyStructures(c);
// 回放玩家改动
const x0=cx*CHUNK, z0=cz*CHUNK;
for(const [ek,id] of edits){
const p=ek.split(',');
const ex=+p[0], ey=+p[1], ez=+p[2];
if(Math.floor(ex/CHUNK)===cx && Math.floor(ez/CHUNK)===cz)
data[IDX(ex-x0, ey, ez-z0)]=id;
}
return c;
}
/* 树:候选列来自本区块及相邻一圈(树冠会越界),按坐标哈希确定性放置 */
function growTrees(c){
const data=c.data, x0=c.cx*CHUNK, z0=c.cz*CHUNK;
function S(wx,wy,wz,id,onlyAir){
const lx=wx-x0, lz=wz-z0;
if(lx<0||lx>=CHUNK||lz<0||lz>=CHUNK||wy<1||wy>=HEIGHT)return;
const i=IDX(lx,wy,lz);
if(onlyAir && data[i]!==AIR)return;
data[i]=id;
}
for(let wz=z0-2; wz<z0+CHUNK+2; wz++)for(let wx=x0-2; wx<x0+CHUNK+2; wx++){
const r=hash2(wx,wz,17);
let cand=false;
if(r<0.006)cand=true;
else if(r<0.036 && moistAt(wx,wz)>0.12)cand=true;
if(!cand)continue;
const h=Math.floor(heightAt(wx,wz));
const topY=h-1;
if(topY<=SEA+1 || topY>=46)continue;
const trunkH=4+((hash2(wx,wz,23)*3)|0);
const base=h;
// 树冠
for(let dy=trunkH-2; dy<=trunkH+1; dy++){
const rad=(dy<=trunkH-1)?2:1;
for(let dx=-rad;dx<=rad;dx++)for(let dz=-rad;dz<=rad;dz++){
if(dx===0&&dz===0&&dy<=trunkH-1)continue;
if(Math.abs(dx)===2&&Math.abs(dz)===2&&hash2(wx*4+dx,wz*4+dz,31)<0.6)continue;
S(wx+dx, base+dy, wz+dz, LEAVES, true);
}
}
for(let y=0;y<trunkH;y++)S(wx, base+y, wz, LOG, false);
}
}
/* ---------------- 神话建筑群 ---------------- */
const REGION=80; // 每 80x80 格一个建筑区域
const PROTECTED=[ // 出生景区保护:强制建筑落点周围不再刷随机建筑
{x:0, z:0, r:52},
{x:26, z:30, r:40},
{x:-58, z:-46,r:44},
{x:-20, z:44, r:36}
];
const FORCED=[
{type:'temple', x:26, z:30 },
{type:'island', x:-58, z:-46, y:84},
{type:'henge', x:-20, z:44 }
];
function structureAt(rx,rz){
if(hash2(rx,rz,41)>0.52)return null;
const x=rx*REGION+40+Math.floor((hash2(rx,rz,42)-0.5)*34);
const z=rz*REGION+40+Math.floor((hash2(rx,rz,43)-0.5)*34);
for(const p of PROTECTED){
const dx=x-p.x, dz=z-p.z;
if(dx*dx+dz*dz < p.r*p.r)return null;
}
const t=hash2(rx,rz,44);
let type;
if(t<0.15)type='temple';
else if(t<0.30)type='henge';
else if(t<0.45)type='tree';
else if(t<0.60)type='spire';
else if(t<0.72)type='gate';
else if(t<0.85)type='island';
else type='ruins';
let y;
if(type==='island'){ y=80+Math.floor(hash2(rx,rz,45)*8); }
else{
y=Math.floor(heightAt(x,z));
if(y<SEA+1 || y>44)return null;
y=Math.max(y, SEA+2);
}
return {type:type, x:x, y:y, z:z};
}
function applyStructures(c){
const x0=c.cx*CHUNK, z0=c.cz*CHUNK;
const W={
S(wx,wy,wz,id){
const lx=wx-x0, lz=wz-z0;
if(lx<0||lx>=CHUNK||lz<0||lz>=CHUNK||wy<0||wy>=HEIGHT)return;
c.data[IDX(lx,wy,lz)]=id;
},
G(wx,wy,wz){
const lx=wx-x0, lz=wz-z0;
if(lx<0||lx>=CHUNK||lz<0||lz>=CHUNK||wy<0||wy>=HEIGHT)return AIR;
return c.data[IDX(lx,wy,lz)];
}
};
const r0x=Math.floor((x0-24)/REGION), r1x=Math.floor((x0+CHUNK-1+24)/REGION);
const r0z=Math.floor((z0-24)/REGION), r1z=Math.floor((z0+CHUNK-1+24)/REGION);
for(let rx=r0x;rx<=r1x;rx++)for(let rz=r0z;rz<=r1z;rz++){
const s=structureAt(rx,rz);
if(s)applyStructure(W,s);
}
for(const f of FORCED){
const s={type:f.type, x:f.x, z:f.z,
y:(f.y!==undefined)?f.y:Math.max(Math.floor(heightAt(f.x,f.z)), SEA+2)};
applyStructure(W,s);
}
}
function applyStructure(W,s){
switch(s.type){
case 'temple': buildTemple(W,s.x,s.y,s.z); break;
case 'henge': buildHenge (W,s.x,s.y,s.z); break;
case 'tree': buildWorldTree(W,s.x,s.y,s.z); break;
case 'spire': buildSpire (W,s.x,s.y,s.z); break;
case 'gate': buildGate (W,s.x,s.y,s.z); break;
case 'island': buildIsland(W,s.x,s.y,s.z); break;
case 'ruins': buildRuins (W,s.x,s.y,s.z); break;
}
}
function buildBox(W,x0,y0,z0,x1,y1,z1,id){
const a=Math.min(x0,x1),b=Math.max(x0,x1),cc=Math.min(y0,y1),d=Math.max(y0,y1),
e=Math.min(z0,z1),f=Math.max(z0,z1);
for(let y=cc;y<=d;y++)for(let z=e;z<=f;z++)for(let x=a;x<=b;x++)W.S(x,y,z,id);
}
/* 把一片地面整平到 y(顶面方块位于 y-1,y 为首个空气层) */
function flatten(W,x0,z0,x1,z1,y){
for(let z=z0;z<=z1;z++)for(let x=x0;x<=x1;x++){
const h=Math.max(1, Math.min(HEIGHT-2, Math.floor(heightAt(x,z))));
const top=Math.min(HEIGHT, Math.max(h+10, y));
for(let yy=y; yy<top; yy++)W.S(x,yy,z,AIR);
for(let yy=h; yy<y-1; yy++)W.S(x,yy,z,DIRT);
if(y-1>=0)W.S(x,y-1,z,GRASS);
}
}
/* 天空神殿:大理石阶梯台基 + 柱廊 + 圣坛 */
function buildTemple(W,bx,by,bz){
flatten(W, bx-11,bz-9, bx+11,bz+9, by);
for(let i=0;i<3;i++)buildBox(W, bx-9+i,by+i,bz-7+i, bx+9-i,by+i,bz+7-i, MARBLE);
const F=by+3; // 殿面首层空气
const xs=[-8,-4,0,4,8], zs=[-6,-3,0,3,6];
for(const ox of xs){column(W,bx+ox,F,bz-6);column(W,bx+ox,F,bz+6);}
for(const oz of zs){column(W,bx-8,F,bz+oz);column(W,bx+8,F,bz+oz);}
buildBox(W, bx-9,F+7,bz-7, bx+9,F+7,bz+7, MARBLE); // 檐部
for(let i=1;i<=4;i++)buildBox(W, bx-9+i,F+7+i,bz-7+i, bx+9-i,F+7+i,bz+7-i, MARBLE); // 叠涩屋顶
for(const sx of [-1,1])for(const sz of [-1,1])
W.S(bx+sx*9, F+8, bz+sz*7, GOLD); // 屋角金饰
// 中央圣坛
W.S(bx,F,bz,GOLD); W.S(bx,F+1,bz,GLOW); W.S(bx,F+2,bz,CRYSTAL);
for(const ox of [-5,5])for(const oz of [-4,4]){
W.S(bx+ox,F,bz+oz,OBSIDIAN); W.S(bx+ox,F+1,bz+oz,GLOW); // 四角火盆
}
function column(W,x,y,z){
buildBox(W, x,y,z, x,y+5,z, MARBLE);
W.S(x,y+6,z,GOLD);
}
}
/* 符文石阵:七柱黑曜石环 + 水晶祭坛 */
function buildHenge(W,bx,by,bz){
flatten(W, bx-11,bz-11, bx+11,bz+11, by);
for(let dz=-10;dz<=10;dz++)for(let dx=-10;dx<=10;dx++){
const d=Math.sqrt(dx*dx+dz*dz);
if(d>5.5&&d<9.5)W.S(bx+dx,by-1,bz+dz,SBRICK);
else if(d<=5.5&&hash2(bx+dx,bz+dz,66)<0.5)W.S(bx+dx,by-1,bz+dz,MOSSY);
}
for(let k=0;k<7;k++){
const a=k/7*Math.PI*2+0.31;
const px=bx+Math.round(Math.cos(a)*7), pz=bz+Math.round(Math.sin(a)*7);
const h=5+((hash2(px,pz,77)*3)|0);
buildBox(W, px,by,pz, px,by+h,pz, OBSIDIAN);
W.S(px,by+2,pz,GLOW);
W.S(px,by+h+1,pz,GLOW);
}
buildBox(W, bx-1,by,bz-1, bx+1,by,bz+1, SBRICK);
W.S(bx,by+1,bz,GOLD); W.S(bx,by+2,bz,CRYSTAL); W.S(bx,by+3,bz,GLOW);
}
/* 世界之树:巨干、根脉、团冠、萤石果实与垂藤 */
function buildWorldTree(W,bx,by,bz){
const H=22+((hash2(bx,bz,91)*6)|0);
for(let y=0;y<H;y++)for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)
W.S(bx+dx,by+y,bz+dz,LOG);
for(let k=0;k<8;k++){
const a=k/8*Math.PI*2+0.39, c=Math.cos(a), s=Math.sin(a);
for(let i=2;i<=5;i++){
const rx=bx+Math.round(c*i), rz=bz+Math.round(s*i);
buildBox(W, rx,by-2,rz, rx,by+Math.max(0,2-i),rz, LOG);
}
}
const blobs=[[0,H+2,0,8],[6,H-2,3,5],[-6,H-1,-2,5],[2,H-4,-6,4],[-3,H-3,6,4]];
for(const bb of blobs){
const ox=bb[0],oy=bb[1],oz=bb[2],r=bb[3];
const steps=Math.max(Math.abs(ox),Math.abs(oz),1)*2;
for(let i=0;i<=steps;i++){
const t=i/steps;
W.S(bx+Math.round(ox*t*0.9), by+H-1+Math.round((oy-H+1)*t), bz+Math.round(oz*t*0.9), LOG);
}
for(let dy=-r;dy<=r;dy++)for(let dz=-r;dz<=r;dz++)for(let dx=-r;dx<=r;dx++){
const d=Math.sqrt(dx*dx+dy*dy*1.4+dz*dz);
if(d>r+0.4)continue;
if(hash2(bx+dx*3+dy, bz+dz*3-dy, 95)<0.12)continue;
const tx=bx+ox+dx, ty=by+oy+dy, tz=bz+oz+dz;
if(W.G(tx,ty,tz)===AIR)W.S(tx,ty,tz,LEAVES);
}
}
let placed=0;
for(let t=0;t<300&&placed<14;t++){
const a=hash2(bx,t,97)*Math.PI*2, rr=3+hash2(t,bx,98)*7;
const tx=bx+Math.round(Math.cos(a)*rr), tz=bz+Math.round(Math.sin(a)*rr);
const ty=by+H-5+((hash2(t,bz,99)*10)|0);
if(W.G(tx,ty,tz)===LEAVES && W.G(tx,ty-1,tz)===AIR){W.S(tx,ty,tz,GLOW);placed++;}
}
for(let k=0;k<26;k++){
const a=hash2(bx,k,101)*Math.PI*2, rr=6+hash2(k,bz,102)*5;
const tx=bx+Math.round(Math.cos(a)*rr), tz=bz+Math.round(Math.sin(a)*rr);
const ty=by+H-2+((hash2(k,bx,103)*6)|0), len=2+((hash2(k,bz,104)*5)|0);
for(let i=0;i<len;i++)if(W.G(tx,ty-i,tz)===AIR)W.S(tx,ty-i,tz,LEAVES);
}
for(let dz=-4;dz<=4;dz++)for(let dx=-4;dx<=4;dx++){
const d=Math.abs(dx)+Math.abs(dz);
if(d>=3&&d<=4&&hash2(bx+dx,bz+dz,105)<0.6){
const gy=Math.floor(heightAt(bx+dx,bz+dz));
W.S(bx+dx,gy-1,bz+dz,MOSSY);
}
}
}
/* 巨龙之塔:石砖高塔 + 外旋梯 + 黑曜尖顶 */
function buildSpire(W,bx,by,bz){
flatten(W, bx-7,bz-7, bx+7,bz+7, by);
const H=20+((hash2(bx,bz,111)*6)|0);
for(let y=0;y<H;y++)for(let dx=-4;dx<=4;dx++)for(let dz=-4;dz<=4;dz++){
if(Math.max(Math.abs(dx),Math.abs(dz))!==4)continue;
W.S(bx+dx, by+y, bz+dz, (hash2(bx+dx, by+y+bz+dz,112)<0.12)?MOSSY:SBRICK);
}
buildBox(W, bx-4,by,bz-4, bx+4,by,bz+4, SBRICK);
buildBox(W, bx-1,by+1,bz+4, bx+1,by+3,bz+4, AIR); // 门
for(const wy of [by+7, by+13])
for(const p of [[-4,0],[4,0],[0,-4],[0,4],[-4,-2],[4,2],[-2,-4],[2,4]])
W.S(bx+p[0], wy, bz+p[1], GLOW);
buildBox(W, bx-4,by+H-1,bz-4, bx+4,by+H-1,bz+4, SBRICK); // 顶板
for(let dx=-4;dx<=4;dx++)for(let dz=-4;dz<=4;dz++){
if(Math.max(Math.abs(dx),Math.abs(dz))!==4)continue;
if(((dx+dz)&1)===0)W.S(bx+dx,by+H,bz+dz,SBRICK); // 城垛
}
buildBox(W, bx-2,by+H+1,bz-2, bx+2,by+H+1,bz+2, OBSIDIAN);
buildBox(W, bx-1,by+H+2,bz-1, bx+1,by+H+3,bz+1, OBSIDIAN);
W.S(bx,by+H+4,bz,OBSIDIAN); W.S(bx,by+H+5,bz,GLOW); W.S(bx,by+H+6,bz,CRYSTAL);
const n=H*2;
for(let i=0;i<n;i++){
const a=i*0.42, r=6;
const sx=bx+Math.round(Math.cos(a)*r), sz=bz+Math.round(Math.sin(a)*r);
const sy=by+1+(i>>1);
W.S(sx,sy,sz,SBRICK);
if(i%6===0)buildBox(W, sx,by,sz, sx,sy-1,sz, SBRICK);
}
}
/* 远古之门:黑曜石门框 + 金饰 + 门前符文环 */
function buildGate(W,bx,by,bz){
flatten(W, bx-8,bz-8, bx+8,bz+8, by);
buildBox(W, bx-7,by,bz-4, bx+7,by,bz+4, SBRICK);
buildBox(W, bx-6,by+1,bz-3, bx+6,by+1,bz+3, SBRICK);
for(const sx of [-4,4]){
buildBox(W, bx+sx-1,by+2,bz-1, bx+sx+1,by+10,bz+1, OBSIDIAN);
W.S(bx+sx,by+11,bz,GOLD);
}
buildBox(W, bx-5,by+11,bz-1, bx+5,by+12,bz+1, OBSIDIAN);
W.S(bx,by+13,bz,GOLD); W.S(bx,by+10,bz,GLOW);
for(const sx of [-7,7]){
W.S(bx+sx,by+1,bz,OBSIDIAN); W.S(bx+sx,by+2,bz,OBSIDIAN); W.S(bx+sx,by+3,bz,GLOW);
}
for(let z=bz+5;z<=bz+8;z++)for(let x=bx-1;x<=bx+1;x++)W.S(x,by-1,z,MARBLE);
for(let dz=-3;dz<=3;dz++)for(let dx=-3;dx<=3;dx++){
const d=Math.sqrt(dx*dx+dz*dz);
if(d>2.2&&d<3.4)W.S(bx+dx,by-1,bz+11+dz,GLOW);
}
}
/* 浮空圣岛:倒锥浮岛 + 瀑布 + 云端圣坛 */
function buildIsland(W,bx,by,bz){
const R=15;
for(let dz=-R;dz<=R;dz++)for(let dx=-R;dx<=R;dx++){
const d=Math.sqrt(dx*dx+dz*dz)/R;
if(d>1)continue;
const tx=bx+dx, tz=bz+dz;
const topY=by+Math.round((1-d)*2.5);
const depth=Math.max(1, Math.round((1-d)*(1-d)*13));
for(let y=topY-depth;y<=topY-3;y++)W.S(tx,y,tz,STONE);
W.S(tx,topY-2,tz,DIRT); W.S(tx,topY-1,tz,DIRT);
W.S(tx,topY,tz,GRASS);
if(d>0.35&&d<0.9&&hash2(tx,tz,121)<0.05){
const len=2+((hash2(tx,tz,122)*3)|0);
for(let i=1;i<=len;i++)W.S(tx,topY-depth-i,tz,CRYSTAL);
}
}
for(let k=0;k<3;k++){
const a=hash2(bx,k+bz,123)*Math.PI*2;
const wx=bx+Math.round(Math.cos(a)*R), wz=bz+Math.round(Math.sin(a)*R);
const ix=bx+Math.round(Math.cos(a)*(R-2)), iz=bz+Math.round(Math.sin(a)*(R-2));
W.S(ix,by+1,iz,WATER);
for(let y=by+1;y>2;y--){
const b=W.G(wx,y-1,wz);
if(b!==AIR&&b!==WATER)break;
W.S(wx,y-1,wz,WATER);
}
}
const F=by+4; // 中心顶面之上
buildBox(W, bx-3,F,bz-3, bx+3,F,bz+3, MARBLE);
for(const p of [[-2,-2],[2,-2],[-2,2],[2,2]]){
buildBox(W, bx+p[0],F+1,bz+p[1], bx+p[0],F+4,bz+p[1], MARBLE);
W.S(bx+p[0],F+5,bz+p[1],GOLD);
}
buildBox(W, bx-3,F+6,bz-3, bx+3,F+6,bz+3, MARBLE);
W.S(bx,F+1,bz,GOLD); W.S(bx,F+2,bz,GLOW); W.S(bx,F+3,bz,CRYSTAL); W.S(bx,F+4,bz,CRYSTAL);
const txx=bx-7, tzz=bz+5;
buildBox(W, txx,by+2,tzz, txx,by+5,tzz, LOG);
for(let dy=0;dy<2;dy++)for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)
if(W.G(txx+dx,by+6+dy,tzz+dz)===AIR)W.S(txx+dx,by+6+dy,tzz+dz,LEAVES);
W.S(txx,by+8,tzz,LEAVES);
}
/* 荒野遗迹:断墙、残柱与藤蔓 */
function buildRuins(W,bx,by,bz){
for(let k=0;k<6;k++){
const ox=((hash2(bx,k,131)*17)|0)-8, oz=((hash2(k,bz,132)*17)|0)-8;
const len=3+((hash2(bx,k+oz,133)*5)|0), horiz=hash2(bx,k,134)<0.5;
const h0=2+((hash2(bx,k,135)*4)|0);
for(let i=0;i<len;i++){
const wx=bx+ox+(horiz?i:0), wz=bz+oz+(horiz?0:i);
const gy=Math.floor(heightAt(wx,wz));
const hh=Math.max(1, h0-((hash2(wx,wz,136)*3)|0));
for(let y=0;y<hh;y++)
W.S(wx,gy+y,wz, (hash2(wx,gy+y,137)<0.3)?MOSSY:COBBLE);
if(hash2(wx,wz,138)<0.3)W.S(wx,gy+hh,wz,LEAVES);
}
}
for(let k=0;k<3;k++){
const wx=bx+((hash2(k,bx,139)*13)|0)-6, wz=bz+((hash2(k,bz,140)*13)|0)-6;
const gy=Math.floor(heightAt(wx,wz));
const hh=2+((hash2(wx,wz,141)*4)|0);
W.S(wx,gy-1,wz,GOLD);
for(let y=0;y<hh;y++)W.S(wx,gy+y,wz,MARBLE);
}
}
/* ---------------- Three.js 场景 ---------------- */
if(!window.THREE){
document.getElementById('startTip').textContent='Three.js 加载失败,请检查网络后刷新';
throw new Error('THREE missing');
}
const SKY=0x9cc4e4;
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(SKY, RD*CHUNK*0.5, RD*CHUNK*0.92);
const camera=new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 900);
camera.rotation.order='YXZ';
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(SKY);
renderer.outputEncoding=THREE.sRGBEncoding;
document.getElementById('game').appendChild(renderer.domElement);
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
const atlasTex=new THREE.CanvasTexture(atlasCanvas);
atlasTex.magFilter=THREE.NearestFilter;
atlasTex.minFilter=THREE.NearestMipmapLinearFilter;
atlasTex.generateMipmaps=true;
atlasTex.encoding=THREE.sRGBEncoding;
const matSolid=new THREE.MeshBasicMaterial({map:atlasTex, vertexColors:true, alphaTest:0.5});
const matWater=new THREE.MeshBasicMaterial({map:atlasTex, vertexColors:true, transparent:true, opacity:0.68, depthWrite:false});
/* 太阳与云 */
const SUN_DIR=new THREE.Vector3(0.45,0.62,0.35).normalize();
const sun=(function(){
const cv=document.createElement('canvas');cv.width=cv.height=128;
const g=cv.getContext('2d');
const gr=g.createRadialGradient(64,64,6,64,64,64);
gr.addColorStop(0,'rgba(255,252,228,1)');
gr.addColorStop(0.32,'rgba(255,242,186,0.95)');
gr.addColorStop(1,'rgba(255,242,186,0)');
g.fillStyle=gr;g.fillRect(0,0,128,128);
const s=new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(cv), fog:false, transparent:true, depthWrite:false}));
s.scale.set(110,110,1); s.renderOrder=3; scene.add(s); return s;
})();
const clouds=[];
(function(){
const cv=document.createElement('canvas');cv.width=128;cv.height=64;
const g=cv.getContext('2d');
const rr=mulberry32(555);
for(let i=0;i<16;i++){
const x=rr()*128, y=18+rr()*30, r=8+rr()*14;
const gr=g.createRadialGradient(x,y,1,x,y,r);
gr.addColorStop(0,'rgba(255,255,255,0.85)');
gr.addColorStop(1,'rgba(255,255,255,0)');
g.fillStyle=gr; g.beginPath(); g.arc(x,y,r,0,7); g.fill();
}
const tex=new THREE.CanvasTexture(cv);
const mat=new THREE.MeshBasicMaterial({map:tex, transparent:true, opacity:0.55, depthWrite:false, side:THREE.DoubleSide});
for(let i=0;i<12;i++){
const m=new THREE.Mesh(new THREE.PlaneGeometry(46+hash2(i,7,203)*46, 18+hash2(i,8,204)*16), mat);
m.rotation.x=-Math.PI/2;
m.position.set((hash2(i,1,201)-0.5)*440, 100, (hash2(i,2,202)-0.5)*440);
m.renderOrder=2;
scene.add(m); clouds.push(m);
}
})();
/* ---------------- 区块网格化(面剔除 + 顶点色明暗) ---------------- */
const FACES=[
{dir:[ 1,0,0], shade:0.80, kind:2, corners:[[1,0,0],[1,1,0],[1,1,1],[1,0,1]]},
{dir:[-1,0,0], shade:0.80, kind:2, corners:[[0,0,1],[0,1,1],[0,1,0],[0,0,0]]},
{dir:[0, 1,0], shade:1.00, kind:0, corners:[[0,1,1],[1,1,1],[1,1,0],[0,1,0]]},
{dir:[0,-1,0], shade:0.50, kind:1, corners:[[0,0,0],[1,0,0],[1,0,1],[0,0,1]]},
{dir:[0,0, 1], shade:0.62, kind:2, corners:[[1,0,1],[1,1,1],[0,1,1],[0,0,1]]},
{dir:[0,0,-1], shade:0.62, kind:2, corners:[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]}
];
const FACE_UV=[[0,0],[0,1],[1,1],[1,0]];
function remesh(c){
disposeMesh(c);
const cx=c.cx, cz=c.cz, data=c.data;
const nx=genChunk(cx+1,cz).data, nz=genChunk(cx,cz+1).data;
const mx=genChunk(cx-1,cz).data, mz=genChunk(cx,cz-1).data;
function gb(x,y,z){
if(y<0)return BEDROCK;
if(y>=HEIGHT)return AIR;
if(x>=0&&x<CHUNK&&z>=0&&z<CHUNK)return data[IDX(x,y,z)];
if(x<0)return mx[IDX(x+CHUNK,y,z)];
if(x>=CHUNK)return nx[IDX(x-CHUNK,y,z)];
if(z<0)return mz[IDX(x,y,z+CHUNK)];
return nz[IDX(x,y,z-CHUNK)];
}
const P=[],U=[],C=[],I=[], WP=[],WU=[],WC=[],WI=[];
let vc=0, wvc=0;
for(let y=0;y<HEIGHT;y++)for(let z=0;z<CHUNK;z++)for(let x=0;x<CHUNK;x++){
const id=data[IDX(x,y,z)];
if(!id)continue;
const b=BLOCKS[id], wat=(id===WATER);
for(let f=0;f<6;f++){
const F=FACES[f];
const nid=gb(x+F.dir[0], y+F.dir[1], z+F.dir[2]);
let emit;
if(wat)emit=(nid===AIR||nid===LEAVES);
else if(nid===AIR||nid===WATER)emit=true;
else emit=(nid===LEAVES&&id!==LEAVES);
if(!emit)continue;
const tile=(F.kind===0)?b.top:((F.kind===1)?b.bottom:b.side);
const uv=tileUV(tile), sh=F.shade;
for(let v=0;v<4;v++){
const cr=F.corners[v];
if(wat){
WP.push(x+cr[0],y+cr[1],z+cr[2]);
WU.push(uv[0]+FACE_UV[v][0]*(uv[2]-uv[0]), uv[1]+FACE_UV[v][1]*(uv[3]-uv[1]));
WC.push(sh,sh,sh);
}else{
P.push(x+cr[0],y+cr[1],z+cr[2]);
U.push(uv[0]+FACE_UV[v][0]*(uv[2]-uv[0]), uv[1]+FACE_UV[v][1]*(uv[3]-uv[1]));
C.push(sh,sh,sh);
}
}
if(wat){WI.push(wvc,wvc+1,wvc+2,wvc,wvc+2,wvc+3);wvc+=4;}
else{I.push(vc,vc+1,vc+2,vc,vc+2,vc+3);vc+=4;}
}
}
c.mesh=makeMesh(P,U,C,I,matSolid,cx*CHUNK,cz*CHUNK,0);
c.wmesh=makeMesh(WP,WU,WC,WI,matWater,cx*CHUNK,cz*CHUNK,1);
c.built=true;
}
function makeMesh(P,U,C,I,mat,x0,z0,order){
if(I.length===0)return null;
const g=new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(P,3));
g.setAttribute('uv', new THREE.Float32BufferAttribute(U,2));
g.setAttribute('color', new THREE.Float32BufferAttribute(C,3));
g.setIndex(I);
const m=new THREE.Mesh(g,mat);
m.position.set(x0,0,z0);
m.renderOrder=order;
m.matrixAutoUpdate=false; m.updateMatrix();
scene.add(m);
return m;
}
function disposeMesh(c){
for(const k of ['mesh','wmesh']){
const m=c[k];
if(m){scene.remove(m); m.geometry.dispose(); c[k]=null;}
}
c.built=false;
}
function setBlockWorld(wx,wy,wz,id){
if(wy<0||wy>=HEIGHT)return;
const cx=Math.floor(wx/CHUNK), cz=Math.floor(wz/CHUNK);
const c=genChunk(cx,cz);
const lx=wx-cx*CHUNK, lz=wz-cz*CHUNK;
c.data[IDX(lx,wy,lz)]=id;
edits.set(wx+','+wy+','+wz, id);
remesh(c);
if(lx===0){const n=chunks.get(ckey(cx-1,cz)); if(n&&n.built)remesh(n);}
if(lx===CHUNK-1){const n=chunks.get(ckey(cx+1,cz)); if(n&&n.built)remesh(n);}
if(lz===0){const n=chunks.get(ckey(cx,cz-1)); if(n&&n.built)remesh(n);}
if(lz===CHUNK-1){const n=chunks.get(ckey(cx,cz+1)); if(n&&n.built)remesh(n);}
}
/* ---------------- 玩家与物理 ---------------- */
const player={x:0.5, y:40, z:0.5, vy:0, yaw:0, pitch:-0.08, fly:false, grounded:false, wasGrounded:false, sel:0};
const spawnPoint={x:0.5, y:40, z:0.5};
function collide(px,py,pz){
const x0=Math.floor(px-0.3), x1=Math.floor(px+0.3);
const y0=Math.floor(py), y1=Math.floor(py+1.8);
const z0=Math.floor(pz-0.3), z1=Math.floor(pz+0.3);
for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++)for(let x=x0;x<=x1;x++)
if(isSolid(getBlock(x,y,z)))return true;
return false;
}
function inWater(){
return getBlock(Math.floor(player.x),Math.floor(player.y+0.3),Math.floor(player.z))===WATER
|| getBlock(Math.floor(player.x),Math.floor(player.y+1.4),Math.floor(player.z))===WATER;
}
function moveAxis(axis,delta){
if(delta===0)return;
const old=player[axis];
player[axis]+=delta;
if(collide(player.x,player.y,player.z)){
player[axis]=old;
if(!player.fly && (player.grounded||player.wasGrounded)){
// 自动登上 1 格台阶
player[axis]=old+delta;
if(!collide(player.x,player.y+0.62,player.z)){
player.y+=0.62;
let drop=0;
while(drop<0.62 && !collide(player.x,player.y-0.05,player.z)){player.y-=0.05;drop+=0.05;}
let guard=0;
while(collide(player.x,player.y,player.z)&&guard++<20)player.y+=0.05;
}else player[axis]=old;
}
}
}
const keys={};
function updatePlayer(dt){
dt=Math.min(dt,0.045);
const wat=inWater();
const fx=-Math.sin(player.yaw), fz=-Math.cos(player.yaw);
const rx=Math.cos(player.yaw), rz=-Math.sin(player.yaw);
let mx=0,mz=0;
if(keys.KeyW){mx+=fx;mz+=fz;}
if(keys.KeyS){mx-=fx;mz-=fz;}
if(keys.KeyD){mx+=rx;mz+=rz;}
if(keys.KeyA){mx-=rx;mz-=rz;}
const len=Math.hypot(mx,mz);
if(len>0){mx/=len;mz/=len;}
const shift=keys.ShiftLeft||keys.ShiftRight;
let speed=player.fly?12:(shift?6.8:4.4);
if(wat&&!player.fly)speed*=0.55;
moveAxis('x', mx*speed*dt);
moveAxis('z', mz*speed*dt);
if(player.fly){
player.vy=((keys.Space?1:0)-(shift?1:0))*12;
}else{
player.vy-=(wat?9:26)*dt;
if(wat){
if(player.vy<-3)player.vy=-3;
if(keys.Space)player.vy=3.4;
}
}
const step=player.vy*dt;
const steps=Math.max(1, Math.ceil(Math.abs(step)/0.25));
player.wasGrounded=player.grounded;
player.grounded=false;
for(let i=0;i<steps;i++){
player.y+=step/steps;
if(collide(player.x,player.y,player.z)){
player.y-=step/steps;
if(player.vy<0)player.grounded=true;
player.vy=0;
break;
}
}
if(player.y<-20)respawn();
camera.position.set(player.x, player.y+1.62, player.z);
camera.rotation.set(player.pitch, player.yaw, 0);
}
function respawn(){
player.x=spawnPoint.x; player.y=spawnPoint.y; player.z=spawnPoint.z;
player.vy=0;
}
/* ---------------- 方块射线(DDA)与破坏/放置 ---------------- */
let curHit=null;
function raycastVoxel(ox,oy,oz,dx,dy,dz,maxDist){
let x=Math.floor(ox), y=Math.floor(oy), z=Math.floor(oz);
const stepX=dx>0?1:-1, stepY=dy>0?1:-1, stepZ=dz>0?1:-1;
const tdX=Math.abs(1/(dx||1e-9)), tdY=Math.abs(1/(dy||1e-9)), tdZ=Math.abs(1/(dz||1e-9));
let tmX=(dx>0?(x+1-ox):(ox-x))*tdX;
let tmY=(dy>0?(y+1-oy):(oy-y))*tdY;
let tmZ=(dz>0?(z+1-oz):(oz-z))*tdZ;
let t=0, nx=0, ny=0, nz=0;
for(let i=0;i<160;i++){
const id=getBlock(x,y,z);
if(isSolid(id))return {x:x,y:y,z:z,nx:nx,ny:ny,nz:nz,id:id};
if(tmX<tmY&&tmX<tmZ){x+=stepX;t=tmX;tmX+=tdX;nx=-stepX;ny=0;nz=0;}
else if(tmY<tmZ){y+=stepY;t=tmY;tmY+=tdY;ny=-stepY;nx=0;nz=0;}
else{z+=stepZ;t=tmZ;tmZ+=tdZ;nz=-stepZ;nx=0;ny=0;}
if(t>maxDist)return null;
}
return null;
}
const HOTBAR=[GRASS,DIRT,STONE,COBBLE,PLANKS,LOG,LEAVES,SAND,GLOW,OBSIDIAN];
function doBreak(){
if(!curHit)return;
if(curHit.id===BEDROCK)return;
setBlockWorld(curHit.x, curHit.y, curHit.z, AIR);
}
function doPlace(){
if(!curHit)return;
const tx=curHit.x+curHit.nx, ty=curHit.y+curHit.ny, tz=curHit.z+curHit.nz;
const cur=getBlock(tx,ty,tz);
if(cur!==AIR&&cur!==WATER)return;
if(tx+1>player.x-0.3&&tx<player.x+0.3&&tz+1>player.z-0.3&&tz<player.z+0.3&&
ty+1>player.y&&ty<player.y+1.8)return; // 不放进自己身体
setBlockWorld(tx,ty,tz,HOTBAR[player.sel]);
}
const highlight=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}));
highlight.visible=false;
scene.add(highlight);
/* ---------------- 输入与指针锁定 ---------------- */
const overlay=document.getElementById('overlay');
const startTip=document.getElementById('startTip');
let locked=false, dragMode=false, ready=false;
let dragBtn=false, dragMoved=0, lastBtn=0;
const held={};
function playing(){return locked||dragMode;}
overlay.addEventListener('click',()=>{
if(!ready)return;
try{
const p=renderer.domElement.requestPointerLock();
if(p&&p.catch)p.catch(()=>{dragMode=true;overlay.classList.add('hidden');});
}catch(e){dragMode=true;overlay.classList.add('hidden');}
});
document.addEventListener('pointerlockchange',()=>{
locked=(document.pointerLockElement===renderer.domElement);
overlay.classList.toggle('hidden', locked);
});
document.addEventListener('pointerlockerror',()=>{
dragMode=true; overlay.classList.add('hidden');
});
function look(dx,dy){
player.yaw-=dx*0.0022;
player.pitch-=dy*0.0022;
player.pitch=Math.max(-1.55, Math.min(1.55, player.pitch));
}
document.addEventListener('mousemove',e=>{
if(locked)look(e.movementX, e.movementY);
else if(dragBtn){dragMoved+=Math.abs(e.movementX)+Math.abs(e.movementY); look(e.movementX, e.movementY);}
});
document.addEventListener('mousedown',e=>{
if(!playing())return;
if(dragMode&&!locked){dragBtn=true; dragMoved=0; lastBtn=e.button; return;}
if(e.button===0)doBreak(); else if(e.button===2)doPlace();
held[e.button]=true;
});
document.addEventListener('mouseup',e=>{
held[e.button]=false;
if(dragMode&&!locked){
dragBtn=false;
if(dragMoved<6){if(lastBtn===0)doBreak(); else if(lastBtn===2)doPlace();}
}
});
setInterval(()=>{
if(locked){if(held[0])doBreak(); else if(held[2])doPlace();}
},240);
document.addEventListener('contextmenu',e=>e.preventDefault());
addEventListener('wheel',e=>{
if(!playing())return;
player.sel=(player.sel+(e.deltaY>0?1:HOTBAR.length-1))%HOTBAR.length;
updateHotbar();
},{passive:true});
addEventListener('keydown',e=>{
if(e.code==='Space')e.preventDefault();
if(e.repeat){keys[e.code]=true; return;}
keys[e.code]=true;
if(e.code==='KeyF'){player.fly=!player.fly; player.vy=0; flash(player.fly?'飞行模式 · 开':'飞行模式 · 关');}
if(e.code==='KeyR')respawn();
if(e.code==='Space'&&!player.fly&&player.grounded){player.vy=8.7; player.grounded=false;}
if(e.code.indexOf('Digit')===0){
const n=+e.code.slice(5);
player.sel=(n===0)?HOTBAR.length-1:Math.min(n-1,HOTBAR.length-1);
updateHotbar();
}
});
addEventListener('keyup',e=>{keys[e.code]=false;});
/* ---------------- HUD ---------------- */
let flashTimer=null;
function flash(t){
const el=document.getElementById('itemName');
el.textContent=t; el.style.opacity=1;
clearTimeout(flashTimer);
flashTimer=setTimeout(()=>{el.style.opacity=0;},1200);
}
function buildHotbar(){
const hb=document.getElementById('hotbar');
HOTBAR.forEach((id,i)=>{
const d=document.createElement('div');
d.className='slot'+(i===0?' sel':'');
const cv=document.createElement('canvas');
cv.width=cv.height=38;
const g=cv.getContext('2d');
g.imageSmoothingEnabled=false;
const tile=BLOCKS[id].side;
g.drawImage(atlasCanvas, (tile%ATLAS_COLS)*TILE, ((tile/ATLAS_COLS)|0)*TILE, TILE, TILE, 1, 1, 36, 36);
d.appendChild(cv);
const k=document.createElement('span');
k.className='k'; k.textContent=(i+1)%10;
d.appendChild(k);
hb.appendChild(d);
});
}
function updateHotbar(){
const slots=document.querySelectorAll('.slot');
slots.forEach((d,i)=>d.classList.toggle('sel', i===player.sel));
flash(BLOCKS[HOTBAR[player.sel]].name);
}
const debugEl=document.getElementById('debug');
let fps=60, fpsCount=0, fpsTime=0;
/* ---------------- 区块流式加载 ---------------- */
let lastPCX=null, lastPCZ=null, pending=[], rescanTimer=0;
function rescan(){
const pcx=Math.floor(player.x/CHUNK), pcz=Math.floor(player.z/CHUNK);
lastPCX=pcx; lastPCZ=pcz;
pending=[];
for(let r=0;r<=RD;r++)for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
if(Math.max(Math.abs(dx),Math.abs(dz))!==r)continue;
const c=chunks.get(ckey(pcx+dx,pcz+dz));
if(!c||!c.built)pending.push([pcx+dx,pcz+dz,r]);
}
pending.sort((a,b)=>a[2]-b[2]);
for(const [k,c] of chunks){
const d=Math.max(Math.abs(c.cx-pcx), Math.abs(c.cz-pcz));
if(d>RD+1&&c.built)disposeMesh(c);
if(d>RD+4)chunks.delete(k);
}
}
function streamWorld(){
const pcx=Math.floor(player.x/CHUNK), pcz=Math.floor(player.z/CHUNK);
if(pcx!==lastPCX||pcz!==lastPCZ)rescan();
else if(++rescanTimer>20){rescanTimer=0; if(pending.length===0)rescan();}
let n=0;
while(pending.length&&n<2){
const p=pending.shift();
const c=genChunk(p[0],p[1]);
if(!c.built)remesh(c);
n++;
}
}
/* ---------------- 初始化与主循环 ---------------- */
const qs=new URLSearchParams(location.search);
let camOverride=null;
if(qs.has('clean'))overlay.style.display='none';
if(qs.get('cam')){
const v=qs.get('cam').split(',').map(Number);
if(v.length>=3&&!isNaN(v[0]))camOverride={x:v[0],y:v[1],z:v[2],yaw:v[3]||0,pitch:v[4]||0};
}
function findSpawn(){
for(let r=0;r<28;r++){
for(let a=0;a<Math.max(1,r*6);a++){
const ang=a/Math.max(1,r*6)*Math.PI*2;
const x=Math.round(Math.cos(ang)*r), z=Math.round(Math.sin(ang)*r);
const h=Math.floor(heightAt(x,z));
if(h-1>SEA+1&&h-1<45)return{x:x+0.5, y:h+0.05, z:z+0.5};
}
}
const h=Math.floor(heightAt(0,0));
return{x:0.5, y:Math.max(h,SEA+2)+0.05, z:0.5};
}
function init(){
const sp=findSpawn();
spawnPoint.x=sp.x; spawnPoint.y=sp.y; spawnPoint.z=sp.z;
player.x=sp.x; player.y=sp.y; player.z=sp.z;
if(camOverride){player.x=camOverride.x; player.z=camOverride.z;}
// 出生即面向天空神殿
player.yaw=Math.atan2(-(26-sp.x), -(30-sp.z));
const pcx=Math.floor(player.x/CHUNK), pcz=Math.floor(player.z/CHUNK);
let rings=[];
for(let r=0;r<=RD;r++)rings.push(r);
let ri=0;
function genRing(){
const r=rings[ri];
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
if(Math.max(Math.abs(dx),Math.abs(dz))!==r)continue;
remesh(genChunk(pcx+dx,pcz+dz));
}
startTip.textContent='正在生成世界…… '+Math.round((ri+1)/rings.length*100)+'%';
ri++;
if(ri<rings.length)setTimeout(genRing,10);
else{
rescan();
startTip.textContent='点击进入世界';
ready=true;
}
}
genRing();
}
buildHotbar();
setTimeout(init,60);
const clock=new THREE.Clock();
let menuT=0;
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.1);
if(ready)streamWorld();
if(playing()){
menuT=0;
updatePlayer(dt);
const dir=camera.getWorldDirection(new THREE.Vector3());
curHit=raycastVoxel(camera.position.x,camera.position.y,camera.position.z, dir.x,dir.y,dir.z, 7);
if(curHit){highlight.visible=true; highlight.position.set(curHit.x+0.5,curHit.y+0.5,curHit.z+0.5);}
else highlight.visible=false;
}else{
highlight.visible=false;
if(camOverride){
camera.position.set(camOverride.x,camOverride.y,camOverride.z);
camera.rotation.set(camOverride.pitch,camOverride.yaw,0);
}else{
menuT+=dt;
player.yaw+=dt*0.045;
camera.position.set(player.x, spawnPoint.y+9+Math.sin(menuT*0.3)*1.5, player.z);
camera.rotation.set(-0.3, player.yaw, 0);
}
}
// 太阳、云、水面呼吸
sun.position.copy(camera.position).addScaledVector(SUN_DIR,600);
for(const m of clouds){
m.position.x+=dt*1.1;
if(m.position.x-camera.position.x>230)m.position.x-=460;
if(m.position.x-camera.position.x<-230)m.position.x+=460;
if(m.position.z-camera.position.z>230)m.position.z-=460;
if(m.position.z-camera.position.z<-230)m.position.z+=460;
}
matWater.opacity=0.64+0.05*Math.sin(performance.now()*0.0013);
renderer.render(scene,camera);
// 调试信息
fpsCount++; fpsTime+=dt;
if(fpsTime>=0.5){fps=Math.round(fpsCount/fpsTime); fpsCount=0; fpsTime=0;
if(ready){
const pcx=Math.floor(player.x/CHUNK), pcz=Math.floor(player.z/CHUNK);
debugEl.textContent='FPS '+fps+' · XYZ '+player.x.toFixed(1)+' / '+player.y.toFixed(1)+' / '+player.z.toFixed(1)
+' · 区块 '+pcx+','+pcz+' · 已加载 '+chunks.size
+(player.fly?'\n飞行模式':'')
+(curHit?('\n准星: '+BLOCKS[curHit.id].name):'');
}
}
}
animate();
</script>
</body>
</html>