Files
nagi-bench/outputs/claude-fable-5/claude-web-app-max/mythos-craft.html
T

1506 lines
69 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MYTHOS CRAFT · 神话境界</title>
<style>
:root{
--gold:#d9b45c; --gold-dim:#9a7d3c; --ink:#0b0e1c; --ink2:#161233; --mist:#aab3d6; --paper:#e9e4d4;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
canvas#game{display:block;position:fixed;inset:0}
/* ---------- Overlay (title / pause) ---------- */
#overlay{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;
background:radial-gradient(120% 90% at 50% 0%, #1d1742 0%, #0c0f22 55%, #070812 100%);
color:var(--paper);text-align:center;padding:24px;overflow-y:auto}
#overlay .frame{max-width:860px;width:100%}
#overlay h1{font-family:Georgia,"Times New Roman","Noto Serif SC",serif;color:var(--gold);
font-size:clamp(34px,6vw,58px);letter-spacing:.14em;font-weight:700;line-height:1.1;
text-shadow:0 0 28px rgba(217,180,92,.35)}
#overlay .sub{margin-top:8px;color:var(--mist);font-size:14px;letter-spacing:.45em;text-transform:uppercase;font-family:ui-monospace,Menlo,Consolas,monospace}
#overlay .lore{margin:18px auto 0;max-width:560px;color:#cfd4ec;font-size:14px;line-height:1.75}
#wonders{margin:26px auto 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;max-width:780px;text-align:left}
#wonders .w{border:1px solid rgba(217,180,92,.22);background:rgba(13,16,36,.55);border-radius:6px;padding:8px 10px}
#wonders .w b{color:var(--gold);font-family:Georgia,"Noto Serif SC",serif;font-size:14px;letter-spacing:.06em}
#wonders .w span{display:block;color:#9aa2c8;font-size:12px;margin-top:2px;line-height:1.5}
#controls{margin:22px auto 0;color:#aab1d4;font-size:13px;line-height:2;max-width:640px}
#controls k{display:inline-block;border:1px solid #3a3f63;border-bottom-width:2px;border-radius:4px;padding:0 7px;margin:0 3px;
font-family:ui-monospace,Menlo,Consolas,monospace;color:var(--paper);background:#191d38;font-size:12px}
#loadwrap{margin:26px auto 0;width:min(420px,80%)}
#loadbar{height:6px;border:1px solid var(--gold-dim);border-radius:3px;overflow:hidden;background:rgba(0,0,0,.4)}
#loadfill{height:100%;width:0%;background:linear-gradient(90deg,var(--gold-dim),var(--gold));transition:width .15s}
#loadtext{margin-top:8px;color:#8d94bb;font-size:12px;letter-spacing:.2em;font-family:ui-monospace,Menlo,Consolas,monospace}
#startbtn{margin-top:22px;padding:13px 46px;font-size:17px;letter-spacing:.3em;cursor:pointer;
font-family:Georgia,"Noto Serif SC",serif;color:var(--gold);background:transparent;
border:1px solid var(--gold);border-radius:4px;transition:all .2s}
#startbtn:disabled{opacity:.35;cursor:default}
#startbtn:not(:disabled):hover{background:rgba(217,180,92,.12);box-shadow:0 0 24px rgba(217,180,92,.35)}
#mobilenote{margin-top:14px;color:#c7793f;font-size:12px;display:none}
/* ---------- HUD ---------- */
#hud{position:fixed;inset:0;z-index:10;pointer-events:none;display:none}
#cross{position:absolute;left:50%;top:50%;width:18px;height:18px;transform:translate(-50%,-50%)}
#cross::before,#cross::after{content:"";position:absolute;background:rgba(255,255,255,.85);mix-blend-mode:difference}
#cross::before{left:8px;top:0;width:2px;height:18px}
#cross::after{left:0;top:8px;width:18px;height:2px}
#info{position:absolute;left:12px;top:10px;color:#e8ebff;font:12px/1.7 ui-monospace,Menlo,Consolas,monospace;
background:rgba(8,10,24,.55);border:1px solid rgba(217,180,92,.25);border-radius:6px;padding:8px 12px;white-space:pre}
#info .gold{color:var(--gold)}
#toast{position:absolute;left:50%;top:64px;transform:translateX(-50%);max-width:520px;text-align:center;
background:rgba(10,12,28,.82);border:1px solid var(--gold);border-radius:8px;padding:12px 22px;
opacity:0;transition:opacity .4s}
#toast b{display:block;color:var(--gold);font-family:Georgia,"Noto Serif SC",serif;font-size:18px;letter-spacing:.12em}
#toast span{display:block;color:#ccd2ef;font-size:13px;margin-top:4px;line-height:1.6}
#hotbar{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:5px;
background:rgba(8,10,24,.55);padding:6px;border-radius:8px;border:1px solid rgba(255,255,255,.12)}
#hotbar .slot{width:48px;height:48px;border:2px solid rgba(255,255,255,.22);border-radius:5px;position:relative;
background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center}
#hotbar .slot.sel{border-color:var(--gold);box-shadow:0 0 12px rgba(217,180,92,.55)}
#hotbar .slot canvas{image-rendering:pixelated;width:36px;height:36px}
#hotbar .slot i{position:absolute;left:3px;top:1px;font-style:normal;font-size:10px;color:#cfd3ea;font-family:ui-monospace,monospace}
#hotbar .slot u{position:absolute;left:0;right:0;bottom:-17px;text-decoration:none;text-align:center;font-size:10px;color:#cfd3ea;opacity:0;transition:opacity .2s}
#hotbar .slot.sel u{opacity:1}
#hint{position:absolute;right:12px;bottom:12px;color:#9aa1c6;font:11px/1.8 ui-monospace,Menlo,Consolas,monospace;text-align:right;
background:rgba(8,10,24,.45);border-radius:6px;padding:7px 11px}
#watermark{position:absolute;right:14px;top:12px;color:rgba(217,180,92,.88);font:600 15px/1.2 ui-monospace,Menlo,Consolas,monospace;
letter-spacing:.06em;pointer-events:none;-webkit-font-smoothing:antialiased;
text-shadow:0 0 1px rgba(0,0,0,.9),0 1px 2px rgba(0,0,0,.75)}
#langswitch{position:absolute;top:14px;right:14px;display:flex;gap:4px;z-index:30}
#langswitch button{padding:5px 11px;font:11px/1 ui-monospace,Menlo,Consolas,monospace;letter-spacing:.08em;cursor:pointer;
color:var(--mist);background:rgba(8,10,24,.65);border:1px solid rgba(217,180,92,.28);border-radius:4px;transition:all .2s}
#langswitch button.on{color:var(--gold);border-color:var(--gold);background:rgba(217,180,92,.1)}
#langswitch button:hover:not(.on){color:var(--paper);border-color:rgba(217,180,92,.5)}
#author{margin-top:20px;color:#7d84a8;font-size:12px;line-height:1.8}
#author a{color:var(--gold);text-decoration:none;border-bottom:1px solid rgba(217,180,92,.35)}
#author a:hover{color:var(--paper);border-bottom-color:var(--gold)}
</style>
</head>
<body>
<div id="langswitch">
<button id="langzh" type="button">中文</button>
<button id="langen" type="button">EN</button>
</div>
<div id="overlay">
<div class="frame">
<h1 id="title">神话境界</h1>
<div class="sub">MYTHOS · CRAFT</div>
<p class="lore" id="lore"></p>
<div id="wonders"></div>
<div id="controls"></div>
<div id="loadwrap">
<div id="loadbar"><div id="loadfill"></div></div>
<div id="loadtext">铸造世界中 … 0%</div>
</div>
<button id="startbtn" disabled>进入神话境界</button>
<div id="mobilenote"></div>
<div id="author"></div>
</div>
</div>
<div id="hud">
<div id="cross"></div>
<div id="watermark">X: @Nag1ovo</div>
<div id="info"></div>
<div id="toast"><b></b><span></span></div>
<div id="hotbar"></div>
<div id="hint"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script id="gamecode">
"use strict";
/* ============================================================
MYTHOS CRAFT —— 体素引擎 + 神话奇观生成器
CORE 段(纯逻辑,可在 Node 中独立测试)
============================================================ */
const SEED=2026, CS=16, CH=100, SEA=26;
/* ---------- 国际化 ---------- */
let LANG=localStorage.getItem('mythos-lang')||(navigator.language.startsWith('zh')?'zh':'en');
const STR={
zh:{
pageTitle:'MYTHOS CRAFT · 神话境界',
title:'神话境界',
lore:'柏林噪声铸成的无垠大地之上,散落着十一处来自诸神话的奇观。<br>破坏与建造皆由你手 —— 按 <b style="color:var(--gold)">G</b> 可在奇观之间巡游。',
controls:'<k>W</k><k>A</k><k>S</k><k>D</k> 移动 · <k>鼠标</k> 环视 · <k>空格</k> 跳跃 · <k>Ctrl</k> 疾跑 · <k>F</k> 飞行(空格升 / Shift 降)<br><k>左键</k> 破坏方块 · <k>右键</k> 放置方块 · <k>1-9 / 滚轮</k> 选择方块 · <k>G</k> 传送至下一处奇观 · <k>[ ]</k> 视距 · <k>Esc</k> 暂停',
hint:'左键 破坏 · 右键 放置 · G 巡游奇观<br>F 飞行 · [ ] 视距 · Esc 暂停',
start:'进入神话境界',
continue:'继 续 探 索',
loading:'铸造世界中 … ',
paused:'已暂停 · 点击按钮重返世界',
ready:'世界已铸成 · 11 处奇观静候探索',
mobile:'⚠ 检测到触屏设备:本作需要键鼠操作,建议使用桌面版 Chrome 体验。',
author:'作者 <a href="https://github.com/Nagi-ovo" target="_blank" rel="noopener">Nagi-ovo</a> · X <a href="https://x.com/Nag1ovo" target="_blank" rel="noopener">@Nag1ovo</a>',
respawnTitle:'✦ 回到出生地',
respawnBody:'虚空之下没有神话。',
bedrockTitle:'基岩',
bedrockBody:'世界的根基,凡力不可摧。',
flyOn:'✦ 飞行模式',
flyOff:'✦ 步行模式',
flyHint:'空格上升 · Shift 下降',
renderDist:'视距 ',
renderDistUnit:' 区块',
dragTitle:'已启用拖拽视角',
dragBody:'按住左键拖动转向 · 点按左键破坏 · 右键放置',
flyEnabled:'(飞行模式已开启)',
discover:'✦ 发现奇观 · ',
nearest:'\n◈ 最近奇观 ',
distUnit:'m ',
hudDist:'视距 ',
hudHold:'手持 ',
hudFlying:' · 飞行中',
hudFps:'FPS ',
},
en:{
pageTitle:'MYTHOS CRAFT · Mythos Realm',
title:'Mythos Realm',
lore:'Across an endless land forged by Perlin noise lie eleven wonders drawn from myth.<br>Break and build as you will — press <b style="color:var(--gold)">G</b> to tour between wonders.',
controls:'<k>W</k><k>A</k><k>S</k><k>D</k> Move · <k>Mouse</k> Look · <k>Space</k> Jump · <k>Ctrl</k> Sprint · <k>F</k> Fly (Space up / Shift down)<br><k>LMB</k> Break · <k>RMB</k> Place · <k>1-9 / Wheel</k> Select block · <k>G</k> Teleport to next wonder · <k>[ ]</k> Render distance · <k>Esc</k> Pause',
hint:'LMB Break · RMB Place · G Tour wonders<br>F Fly · [ ] Render dist · Esc Pause',
start:'Enter Mythos Realm',
continue:'Continue Exploring',
loading:'Forging world … ',
paused:'Paused · Click to return',
ready:'World forged · 11 wonders await',
mobile:'⚠ Touch device detected: keyboard & mouse required. Desktop Chrome recommended.',
author:'By <a href="https://github.com/Nagi-ovo" target="_blank" rel="noopener">Nagi-ovo</a> · X <a href="https://x.com/Nag1ovo" target="_blank" rel="noopener">@Nag1ovo</a>',
respawnTitle:'✦ Returned to spawn',
respawnBody:'No myth lies beneath the void.',
bedrockTitle:'Bedrock',
bedrockBody:'The foundation of the world — beyond mortal strength.',
flyOn:'✦ Flight mode',
flyOff:'✦ Walk mode',
flyHint:'Space up · Shift down',
renderDist:'Render distance ',
renderDistUnit:' chunks',
dragTitle:'Drag-look enabled',
dragBody:'Hold LMB to turn · Tap LMB to break · RMB to place',
flyEnabled:' (flight mode on)',
discover:'✦ Wonder found · ',
nearest:'\n◈ Nearest wonder ',
distUnit:'m ',
hudDist:'Dist ',
hudHold:'Holding ',
hudFlying:' · Flying',
hudFps:'FPS ',
}
};
function S(k){return STR[LANG][k];}
function wName(w){return LANG==='en'?w.nameEn:w.name;}
function wLore(w){return LANG==='en'?w.loreEn:w.lore;}
function bName(id){const b=BLOCKS[id];return LANG==='en'?(b.nEn||b.n):b.n;}
function setLang(lang){
LANG=lang;
localStorage.setItem('mythos-lang',lang);
applyLang();
}
function applyLang(){
document.documentElement.lang=LANG==='zh'?'zh-CN':'en';
document.title=S('pageTitle');
document.getElementById('title').textContent=S('title');
document.getElementById('lore').innerHTML=S('lore');
document.getElementById('controls').innerHTML=S('controls');
document.getElementById('hint').innerHTML=S('hint');
document.getElementById('mobilenote').textContent=S('mobile');
document.getElementById('author').innerHTML=S('author');
document.getElementById('langzh').classList.toggle('on',LANG==='zh');
document.getElementById('langen').classList.toggle('on',LANG==='en');
fillWonders();
updateHotbarNames();
if(typeof state==='undefined')return;
if(state==='ready'){
document.getElementById('startbtn').textContent=S('start');
document.getElementById('loadtext').textContent=S('ready');
}else if(state==='paused'){
document.getElementById('startbtn').textContent=S('continue');
document.getElementById('loadtext').textContent=S('paused');
}else if(state==='loading'){
const pct=loadList.length?Math.round(100*loadIdx/loadList.length):0;
document.getElementById('loadtext').textContent=S('loading')+pct+'%';
}
}
/* ---------- 基础工具 ---------- */
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
function lerp(a,b,t){return a+(b-a)*t;}
function smoothstep(a,b,x){const t=clamp((x-a)/(b-a),0,1);return t*t*(3-2*t);}
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){let h=Math.imul(x,374761393)+Math.imul(z,668265263)+Math.imul(SEED,144665);h=Math.imul(h^(h>>>13),1274126177);h^=h>>>16;return(h>>>0)/4294967296;}
function hash3(x,y,z){let h=Math.imul(x,374761393)+Math.imul(y,1103515245)+Math.imul(z,668265263)+SEED;h=Math.imul(h^(h>>>13),1274126177);h^=h>>>16;return(h>>>0)/4294967296;}
/* ---------- 种子化柏林噪声(2D / 3D) ---------- */
function makeNoise(seed){
const rand=mulberry32(seed);
const perm=new Array(256);for(let i=0;i<256;i++)perm[i]=i;
for(let i=255;i>0;i--){const j=Math.floor(rand()*(i+1));const t=perm[i];perm[i]=perm[j];perm[j]=t;}
const p=new Uint8Array(512);for(let i=0;i<512;i++)p[i]=perm[i&255];
const fade=t=>t*t*t*(t*(t*6-15)+10);
function g2(h,x,y){switch(h&7){case 0:return x+y;case 1:return -x+y;case 2:return x-y;case 3:return -x-y;case 4:return x;case 5:return -x;case 6:return y;default:return -y;}}
function g3(h,x,y,z){const g=h&15;const u=g<8?x:y;const v=g<4?y:(g===12||g===14?x:z);return((g&1)===0?u:-u)+((g&2)===0?v:-v);}
function n2(x,y){
const X=Math.floor(x)&255,Y=Math.floor(y)&255;
x-=Math.floor(x);y-=Math.floor(y);
const u=fade(x),v=fade(y);
const A=p[X]+Y,Bx=p[X+1]+Y;
return lerp(lerp(g2(p[A],x,y),g2(p[Bx],x-1,y),u),
lerp(g2(p[A+1],x,y-1),g2(p[Bx+1],x-1,y-1),u),v);
}
function n3(x,y,z){
const X=Math.floor(x)&255,Y=Math.floor(y)&255,Z=Math.floor(z)&255;
x-=Math.floor(x);y-=Math.floor(y);z-=Math.floor(z);
const u=fade(x),v=fade(y),w=fade(z);
const A=p[X]+Y,AA=p[A]+Z,AB=p[A+1]+Z,Bx=p[X+1]+Y,BA=p[Bx]+Z,BB=p[Bx+1]+Z;
return lerp(
lerp(lerp(g3(p[AA],x,y,z),g3(p[BA],x-1,y,z),u),
lerp(g3(p[AB],x,y-1,z),g3(p[BB],x-1,y-1,z),u),v),
lerp(lerp(g3(p[AA+1],x,y,z-1),g3(p[BA+1],x-1,y,z-1),u),
lerp(g3(p[AB+1],x,y-1,z-1),g3(p[BB+1],x-1,y-1,z-1),u),v),w);
}
return{n2,n3};
}
const NZ=makeNoise(SEED);
function fbm2(x,y,oct,lac,gain){let a=1,f=1,s=0,n=0;for(let i=0;i<oct;i++){s+=a*NZ.n2(x*f,y*f);n+=a;a*=gain;f*=lac;}return s/n;}
/* ---------- 方块注册表 ---------- */
const B={AIR:0,GRASS:1,DIRT:2,STONE:3,SAND:4,LOG:5,LEAF:6,MYTHLEAF:7,SNOW:8,WATER:9,
QUARTZ:10,PILLAR:11,GOLD:12,BRICK:13,MOSSY:14,SANDSTONE:15,OBSIDIAN:16,GLOW:17,
CRYSTAL:18,BONE:19,RED:20,SLATE:21,PLANK:22,BEDROCK:23,GOLDORE:24};
/* 纹理图集 tile 编号(8 列 × 4 行,每格 16px) */
const T={grassTop:0,grassSide:1,dirt:2,stone:3,sand:4,logSide:5,logTop:6,leaf:7,
mythLeaf:8,snow:9,water:10,quartz:11,pillarSide:12,pillarTop:13,gold:14,goldOre:15,
brick:16,mossy:17,sandstone:18,obsidian:19,glow:20,crystal:21,bone:22,red:23,
slate:24,plank:25,bedrock:26};
const BLOCKS=[];
BLOCKS[B.GRASS] ={n:"草方块", nEn:"Grass", t:T.grassTop, s:T.grassSide, b:T.dirt};
BLOCKS[B.DIRT] ={n:"泥土", nEn:"Dirt", t:T.dirt, s:T.dirt, b:T.dirt};
BLOCKS[B.STONE] ={n:"石头", nEn:"Stone", t:T.stone, s:T.stone, b:T.stone};
BLOCKS[B.SAND] ={n:"沙子", nEn:"Sand", t:T.sand, s:T.sand, b:T.sand};
BLOCKS[B.LOG] ={n:"原木", nEn:"Log", t:T.logTop, s:T.logSide, b:T.logTop};
BLOCKS[B.LEAF] ={n:"树叶", nEn:"Leaves", t:T.leaf, s:T.leaf, b:T.leaf};
BLOCKS[B.MYTHLEAF] ={n:"神木树冠",nEn:"Myth Leaves",t:T.mythLeaf, s:T.mythLeaf, b:T.mythLeaf};
BLOCKS[B.SNOW] ={n:"雪", nEn:"Snow", t:T.snow, s:T.snow, b:T.snow};
BLOCKS[B.WATER] ={n:"水", nEn:"Water", t:T.water, s:T.water, b:T.water,water:true};
BLOCKS[B.QUARTZ] ={n:"白石英", nEn:"Quartz", t:T.quartz, s:T.quartz, b:T.quartz};
BLOCKS[B.PILLAR] ={n:"石英柱", nEn:"Pillar", t:T.pillarTop,s:T.pillarSide,b:T.pillarTop};
BLOCKS[B.GOLD] ={n:"黄金块", nEn:"Gold", t:T.gold, s:T.gold, b:T.gold};
BLOCKS[B.BRICK] ={n:"石砖", nEn:"Brick", t:T.brick, s:T.brick, b:T.brick};
BLOCKS[B.MOSSY] ={n:"苔石砖", nEn:"Mossy Brick",t:T.mossy, s:T.mossy, b:T.mossy};
BLOCKS[B.SANDSTONE]={n:"砂岩", nEn:"Sandstone", t:T.sandstone,s:T.sandstone, b:T.sandstone};
BLOCKS[B.OBSIDIAN] ={n:"黑曜石", nEn:"Obsidian", t:T.obsidian, s:T.obsidian, b:T.obsidian};
BLOCKS[B.GLOW] ={n:"萤石", nEn:"Glowstone", t:T.glow, s:T.glow, b:T.glow,glow:true};
BLOCKS[B.CRYSTAL] ={n:"秘晶", nEn:"Crystal", t:T.crystal, s:T.crystal, b:T.crystal,glow:true};
BLOCKS[B.BONE] ={n:"龙骨", nEn:"Bone", t:T.bone, s:T.bone, b:T.bone};
BLOCKS[B.RED] ={n:"朱红木", nEn:"Red Wood", t:T.red, s:T.red, b:T.red};
BLOCKS[B.SLATE] ={n:"玄岩", nEn:"Slate", t:T.slate, s:T.slate, b:T.slate};
BLOCKS[B.PLANK] ={n:"木板", nEn:"Planks", t:T.plank, s:T.plank, b:T.plank};
BLOCKS[B.BEDROCK] ={n:"基岩", nEn:"Bedrock", t:T.bedrock, s:T.bedrock, b:T.bedrock};
BLOCKS[B.GOLDORE] ={n:"金矿石", nEn:"Gold Ore", t:T.goldOre, s:T.goldOre, b:T.goldOre};
const HOTBAR=[B.GRASS,B.STONE,B.LOG,B.PLANK,B.BRICK,B.QUARTZ,B.GLOW,B.GOLD,B.CRYSTAL];
/* ============================================================
地形生成
============================================================ */
const BIO={PLAINS:0,FOREST:1,DESERT:2};
function heightAt(x,z){
const cont =fbm2(x*0.0022+100, z*0.0022-60, 4,2,0.5);
const hills=fbm2(x*0.012 -40, z*0.012 +90, 4,2,0.5);
let ridge=1-Math.abs(NZ.n2(x*0.004+500, z*0.004+500)); ridge*=ridge;
const mMask=smoothstep(0.25,0.75,(fbm2(x*0.0035-200, z*0.0035+300, 3,2,0.5)+1)/2);
let h=30+cont*14+hills*6+ridge*34*mMask;
const d=Math.sqrt(x*x+z*z);
const ocean=smoothstep(-0.06,-0.34,cont)*smoothstep(175,215,d); // 展示区外:大陆噪声强负 → 海盆
h=lerp(h, 13.5+hills*2.0, ocean);
if(d<160){ h=lerp(33+hills*2.0, h, smoothstep(60,160,d)); } // 出生地高原:展示区平整且不被淹没
return clamp(Math.floor(h),4,CH-10);
}
function biomeAt(x,z){
const t=fbm2(x*0.003+900, z*0.003-700, 3,2,0.5);
const m=fbm2(x*0.003-300, z*0.003+800, 3,2,0.5);
if(t>0.25&&m<-0.05)return BIO.DESERT;
if(m>0.12)return BIO.FOREST;
return BIO.PLAINS;
}
function caveAt(x,y,z){
const a=NZ.n3(x*0.05, y*0.085, z*0.05);
const b=NZ.n3(x*0.022+400, y*0.045+100, z*0.022-400);
return a>0.46&&b>0.18;
}
/* 天空浮岛(距出生点 170 格外的高空) */
function skyIslandColumn(x,z){
if(x*x+z*z<170*170)return null;
const m=(fbm2(x*0.0085+5000, z*0.0085-5000, 3,2,0.5)+1)/2;
if(m<0.73)return null;
const t=(m-0.73)/0.27;
const base=72+Math.floor(fbm2(x*0.02-3000, z*0.02+3000, 2,2,0.5)*5);
return{top:Math.min(base+Math.floor(t*7),CH-4), depth:2+Math.floor(t*9)};
}
/* ---------- 区块守护写入器 ---------- */
function makeSetter(data,cx,cz){
const x0=cx*CS,z0=cz*CS;
return function set(x,y,z,id,soft){
x=Math.round(x);y=Math.round(y);z=Math.round(z);
if(y<0||y>=CH)return;
const lx=x-x0,lz=z-z0;
if(lx<0||lx>=CS||lz<0||lz>=CS)return;
const i=(y*CS+lz)*CS+lx;
if(soft&&data[i]!==B.AIR)return;
data[i]=id;
};
}
/* ---------- 树木 ---------- */
function stampTree(set,x,gy,z,r01){
const th=4+Math.floor(r01*3);
for(let i=1;i<=th;i++)set(x,gy+i,z,B.LOG);
const ty=gy+th;
for(let dy=-2;dy<=2;dy++)for(let dz=-2;dz<=2;dz++)for(let dx=-2;dx<=2;dx++){
const ad=Math.abs(dx)+Math.abs(dz)+Math.abs(dy)*1.5;
if(ad>3.6)continue;
if(dx===0&&dz===0&&dy<=0)continue;
if(hash3(x+dx,ty+dy,z+dz)<0.12)continue;
set(x+dx,ty+dy,z+dz,B.LEAF,true);
}
}
/* ============================================================
神话奇观 —— 十一座固定地标 + 程序化网格播种
============================================================ */
const WONDERS=[
{t:"temple", x:55, z:-35, name:"奥林匹斯神殿", nameEn:"Temple of Olympus", lore:"白石与黄金筑成的众神居所,圣火长明不熄。", loreEn:"White stone and gold halls of the gods, sacred flame eternal."},
{t:"tree", x:-62, z:-48, name:"世界之树", nameEn:"World Tree", lore:"根系贯穿九界,枝头垂着发光的智慧之果。", loreEn:"Roots span nine realms; luminous fruits of wisdom hang from its branches."},
{t:"ziggurat", x:-18, z:-118, name:"通天巴别塔", nameEn:"Tower of Babel", lore:"七层高台直指苍穹,顶端的圣坛彻夜燃烧。", loreEn:"Seven tiers reach skyward; the altar atop burns through the night."},
{t:"pyramid", x:96, z:62, name:"法老金字塔", nameEn:"Pharaoh's Pyramid", lore:"黄金顶冠之下,沉睡着装满宝藏的密室。", loreEn:"Beneath the golden cap sleeps a treasure-filled chamber."},
{t:"henge", x:-72, z:42, name:"德鲁伊巨石阵", nameEn:"Druid Stone Henge", lore:"环形巨石在月光下低声吟唱古老咒文。", loreEn:"Standing stones whisper ancient spells beneath the moon."},
{t:"dragon", x:132, z:-8, name:"巨龙遗骸", nameEn:"Dragon Remains", lore:"上古巨龙陨落于此,眼窝中余烬未熄。", loreEn:"An ancient dragon fell here; embers still glow in its eye sockets."},
{t:"wizard", x:26, z:78, name:"贤者之塔", nameEn:"Wizard's Tower", lore:"塔顶星辉永不熄灭,碎石环绕悬浮。", loreEn:"Starlight never fades atop the tower; stones orbit in suspension."},
{t:"torii", x:-15, z:100, name:"千本鸟居", nameEn:"Thousand Torii", lore:"朱红门廊连成参道,通往神明栖居之地。", loreEn:"Crimson gates form a path to where spirits dwell."},
{t:"portal", x:14, z:-58, name:"黑曜石遗门", nameEn:"Obsidian Gate", lore:"破碎的门框里残留着另一个世界的回声。", loreEn:"A shattered frame echoes remnants of another world."},
{t:"crystal", x:-110,z:-6, name:"水晶秘境", nameEn:"Crystal Cavern", lore:"大地裂隙中生出会发光的巨大晶簇。", loreEn:"Giant luminous crystals grow from a rift in the earth."},
{t:"skyshrine",x:34, z:-86, name:"浮空圣殿", nameEn:"Sky Shrine", lore:"悬于云端的孤岛圣殿,水晶如瀑布垂落。", loreEn:"An island sanctuary in the clouds; crystals cascade like waterfalls.", y:78}
];
const STRUCT_R={temple:26,tree:18,ziggurat:24,pyramid:22,henge:16,dragon:24,wizard:14,torii:18,portal:11,crystal:14,skyshrine:16};
const GRID_POOL=["tree","tree","tree","henge","henge","wizard","wizard","torii","portal","crystal","crystal","ziggurat","temple","pyramid","dragon"];
function collectStructures(cx,cz){
const out=[];
const x0=cx*CS, z0=cz*CS, x1=x0+CS-1, z1=z0+CS-1;
for(const w of WONDERS){
const r=STRUCT_R[w.t];
if(w.x+r>=x0&&w.x-r<=x1&&w.z+r>=z0&&w.z-r<=z1)
out.push({t:w.t,x:w.x,z:w.z,y:w.y,seed:(Math.imul(w.x,73856093)^Math.imul(w.z,19349663))>>>0});
}
const G=112,M=34;
const gx0=Math.floor((x0-48)/G), gx1=Math.floor((x1+48)/G);
const gz0=Math.floor((z0-48)/G), gz1=Math.floor((z1+48)/G);
for(let gz=gz0;gz<=gz1;gz++)for(let gx=gx0;gx<=gx1;gx++){
if(hash2(gx*7349+13,gz*9157+71)<0.42)continue;
const t=GRID_POOL[Math.floor(hash2(gx*131+7,gz*177+3)*GRID_POOL.length)%GRID_POOL.length];
const px=gx*G+M+Math.floor(hash2(gx*31+1,gz*57+9)*(G-2*M));
const pz=gz*G+M+Math.floor(hash2(gx*91+5,gz*113+2)*(G-2*M));
if(px*px+pz*pz<190*190)continue; // 展示区净空
if(heightAt(px,pz)<=SEA+1)continue; // 不在水里播种
const r=STRUCT_R[t];
if(px+r<x0||px-r>x1||pz+r<z0||pz-r>z1)continue;
out.push({t,x:px,z:pz,seed:(Math.imul(px,73856093)^Math.imul(pz,19349663))>>>0});
}
return out;
}
/* ---------- 建造上下文 ---------- */
function makeCtx(set,s){
const gy=(s.y!==undefined)?s.y:heightAt(s.x,s.z);
const c={
x:s.x, z:s.z, gy, set,
rng:mulberry32((s.seed||1)+7),
fill(x0,y0,z0,x1,y1,z1,id,soft){
if(x1<x0){const t=x0;x0=x1;x1=t;} if(y1<y0){const t=y0;y0=y1;y1=t;} if(z1<z0){const t=z0;z0=z1;z1=t;}
y0=Math.max(0,y0); y1=Math.min(CH-1,y1);
for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++)for(let x=x0;x<=x1;x++)set(x,y,z,id,soft);
},
disc(cx,y,cz,r,id,ring){
const r2=r*r, ir2=ring?(r-1.3)*(r-1.3):-1;
for(let dz=-r;dz<=r;dz++)for(let dx=-r;dx<=r;dx++){
const d2=dx*dx+dz*dz;
if(d2<=r2&&d2>ir2)set(cx+dx,y,cz+dz,id);
}
},
sphere(cx,cy,cz,r,id,holeP){
for(let dy=-r;dy<=r;dy++)for(let dz=-r;dz<=r;dz++)for(let dx=-r;dx<=r;dx++){
if(dx*dx+dy*dy+dz*dz>r*r+0.5)continue;
if(holeP&&hash3(cx+dx,cy+dy,cz+dz)<holeP)continue;
set(cx+dx,cy+dy,cz+dz,id,true);
}
},
col(x,z,y0,y1,id){for(let y=y0;y<=y1;y++)set(x,y,z,id);},
pile(x,z,yTop,id){const g=heightAt(x,z);for(let y=Math.max(1,g-4);y<=yTop;y++)set(x,y,z,id);}
};
return c;
}
/* ---------- 十一位建造者 ---------- */
const BUILDERS={
temple(c){ // 奥林匹斯神殿
const{x,z,gy,set}=c;
c.fill(x-15,gy+1,z-11,x+15,gy+20,z+11,B.AIR);
c.fill(x-13,gy-5,z-9,x+13,gy,z+9,B.BRICK);
c.fill(x-12,gy+1,z-8,x+12,gy+1,z+8,B.QUARTZ);
c.fill(x-11,gy+2,z-7,x+11,gy+2,z+7,B.QUARTZ);
c.fill(x-10,gy+3,z-6,x+10,gy+3,z+6,B.QUARTZ);
const fy=gy+4;
for(let k=-9;k<=9;k+=3){ c.col(x+k,z-5,fy,fy+4,B.PILLAR); c.col(x+k,z+5,fy,fy+4,B.PILLAR); }
for(let k=-3;k<=3;k+=2){ c.col(x-9,z+k,fy,fy+4,B.PILLAR); c.col(x+9,z+k,fy,fy+4,B.PILLAR); }
c.fill(x-10,fy+5,z-6,x+10,fy+5,z+6,B.QUARTZ); // 檐板
for(let k=0;k<=3;k++){ // 双坡屋顶
const zz=6-k*2;
if(zz<0)break;
for(const sg of[-1,1])for(let q=0;q<2;q++){
if(zz-q<0)continue;
c.fill(x-10,fy+6+k,z+sg*(zz-q),x+10,fy+6+k,z+sg*(zz-q),B.QUARTZ);
}
c.fill(x-10,fy+6+k,z-(6-2*k),x-10,fy+6+k,z+(6-2*k),B.QUARTZ); // 山形墙
c.fill(x+10,fy+6+k,z-(6-2*k),x+10,fy+6+k,z+(6-2*k),B.QUARTZ);
}
c.fill(x-10,fy+9,z,x+10,fy+9,z,B.GOLD); // 金脊
set(x-10,fy+10,z,B.GOLD); set(x+10,fy+10,z,B.GOLD);
c.fill(x-1,fy,z-1,x+1,fy,z+1,B.GOLD); // 圣坛
set(x,fy+1,z,B.QUARTZ); set(x,fy+2,z,B.GLOW);
for(const sg of[-1,1]){ // 入口火盆
set(x-12,gy+3,z+sg*6,B.SLATE); set(x-12,gy+4,z+sg*6,B.GLOW);
set(x+12,gy+3,z+sg*6,B.SLATE); set(x+12,gy+4,z+sg*6,B.GLOW);
}
},
tree(c){ // 世界之树
const{x,z,gy,set,rng}=c;
const H=30;
c.fill(x-10,gy+1,z-10,x+10,gy+H+11,z+10,B.AIR);
for(let i=0;i<5;i++){ // 盘根
const ang=i/5*Math.PI*2+rng()*0.5;
for(let d=2;d<=6;d++){
const rx=Math.round(x+Math.cos(ang)*d), rz=Math.round(z+Math.sin(ang)*d);
const ry=gy+Math.max(0,3-Math.floor(d/2));
c.col(rx,rz,Math.max(1,ry-5),ry,B.LOG);
}
}
for(let h=0;h<=H;h++){ // 渐细树干
const r=Math.max(1,Math.round(3.2-(h/H)*2.2));
const wob=Math.round(Math.sin(h*0.4)*0.8);
c.disc(x+wob,gy+h,z,r,B.LOG);
}
for(let i=0;i<6;i++){ // 黄金角度分枝
const ang=i/6*Math.PI*2+rng()*0.6;
const by=gy+16+Math.floor(rng()*9);
const len=5+Math.floor(rng()*3);
let bx=x,bz=z,byy=by;
for(let d=1;d<=len;d++){
bx=x+Math.cos(ang)*d; bz=z+Math.sin(ang)*d; byy=by+Math.floor(d*0.5);
set(Math.round(bx),byy,Math.round(bz),B.LOG);
}
c.sphere(Math.round(bx),byy+1,Math.round(bz),3,B.MYTHLEAF,0.15);
}
c.sphere(x,gy+H+2,z,8,B.MYTHLEAF,0.13); // 主树冠
c.sphere(x,gy+H+5,z,5,B.MYTHLEAF,0.13);
for(let i=0;i<14;i++){ // 发光果实
const a=rng()*Math.PI*2, rr=3+rng()*5;
set(Math.round(x+Math.cos(a)*rr),gy+H-1+Math.floor(rng()*6),Math.round(z+Math.sin(a)*rr),B.GLOW,true);
}
c.disc(x,gy+1,z,5,B.MOSSY,true); // 苔石根环
},
ziggurat(c){ // 通天巴别塔
const{x,z,gy,set}=c;
c.fill(x-14,gy+1,z-14,x+14,gy+40,z+14,B.AIR);
c.fill(x-13,gy-5,z-13,x+13,gy,z+13,B.SANDSTONE);
let half=12, y=gy+1;
for(let tier=0;tier<7;tier++){
c.fill(x-half,y,z-half,x+half,y+3,z+half,B.SANDSTONE);
for(let k=-half;k<=half;k++){
set(x+k,y+3,z-half,B.BRICK); set(x+k,y+3,z+half,B.BRICK);
set(x-half,y+3,z+k,B.BRICK); set(x+half,y+3,z+k,B.BRICK);
}
c.fill(x-1,y,z-half,x+1,y+1,z-half,B.SLATE); // 各面壁龛
c.fill(x-1,y,z+half,x+1,y+1,z+half,B.SLATE);
c.fill(x-half,y,z-1,x-half,y+1,z+1,B.SLATE);
c.fill(x+half,y,z-1,x+half,y+1,z+1,B.SLATE);
y+=4; half=Math.max(2,half-2);
}
for(const[dx,dz]of[[-2,-2],[-2,2],[2,-2],[2,2]])c.col(x+dx,z+dz,y,y+3,B.PILLAR);
c.fill(x-2,y+4,z-2,x+2,y+4,z+2,B.QUARTZ);
set(x,y,z,B.GOLD); set(x,y+1,z,B.GLOW); set(x,y+5,z,B.GLOW);
},
pyramid(c){ // 法老金字塔
const{x,z,gy,set}=c;
const half=12;
c.fill(x-half-2,gy+1,z-half-2,x+half+2,gy+18,z+half+2,B.AIR);
c.fill(x-half,gy-5,z-half,x+half,gy,z+half,B.SANDSTONE);
let y=gy+1, hh=half;
while(hh>=0){ c.fill(x-hh,y,z-hh,x+hh,y,z+hh,B.SANDSTONE); y++; hh--; }
const apex=y-1;
c.fill(x-1,apex-1,z-1,x+1,apex-1,z+1,B.GOLD); // 黄金顶冠
set(x,apex,z,B.GOLD);
c.fill(x-3,gy+2,z-3,x+3,gy+5,z+3,B.AIR); // 宝藏密室
c.fill(x-3,gy+1,z-3,x+3,gy+1,z+3,B.GOLD);
set(x,gy+2,z,B.GOLD); set(x,gy+3,z,B.GLOW);
for(const[dx,dz]of[[-3,-3],[-3,3],[3,-3],[3,3]])set(x+dx,gy+2,z+dz,B.GLOW);
c.fill(x-1,gy+2,z-half-1,x+1,gy+3,z-3,B.AIR); // 北侧走廊
c.fill(x-1,gy+1,z-half-1,x+1,gy+1,z-4,B.SANDSTONE);
},
henge(c){ // 德鲁伊巨石阵
const{x,z,gy,set,rng}=c;
c.fill(x-13,gy+1,z-13,x+13,gy+9,z+13,B.AIR);
for(let yy=gy-3;yy<gy;yy++)c.disc(x,yy,z,12,B.DIRT);
c.disc(x,gy,z,12,B.GRASS);
for(let i=0;i<8;i++){
const a=i/8*Math.PI*2;
const px=Math.round(x+Math.cos(a)*9), pz=Math.round(z+Math.sin(a)*9);
const horiz=Math.abs(Math.cos(a))>Math.abs(Math.sin(a));
const ox=horiz?0:1, oz=horiz?1:0;
c.col(px-ox,pz-oz,gy+1,gy+4,B.STONE);
c.col(px+ox,pz+oz,gy+1,gy+4,B.STONE);
for(let k=-1;k<=1;k++)set(px+ox*k,gy+5,pz+oz*k,B.STONE);
}
for(let i=0;i<5;i++){
const a=i/5*Math.PI*2+0.4;
const px=Math.round(x+Math.cos(a)*4.5), pz=Math.round(z+Math.sin(a)*4.5);
c.col(px,pz,gy+1,gy+2+Math.floor(rng()*2),B.MOSSY);
}
set(x,gy+1,z,B.SLATE); set(x,gy+2,z,B.CRYSTAL);
},
dragon(c){ // 巨龙遗骸
const{x,z,gy,set}=c;
c.fill(x-16,gy+1,z-7,x+19,gy+12,z+7,B.AIR);
const L=26;
for(let i=0;i<=L;i++){ // 脊椎抛物线
const sx=x-13+i;
const sy=Math.max(gy,heightAt(sx,z))+1+Math.round(Math.sin(i/L*Math.PI)*6);
set(sx,sy,z,B.BONE);
if(i>=4&&i<=20&&i%3===0){ // 肋骨
for(const sg of[-1,1])for(let r=1;r<=4;r++){
const drop=[0,0,1,3][r-1];
set(sx,sy-drop,z+sg*r,B.BONE);
}
}
}
const hx=x+13, hy=gy+2; // 头骨
c.fill(hx,hy,z-2,hx+4,hy+3,z+2,B.BONE);
c.fill(hx+1,hy+1,z-1,hx+5,hy+1,z+1,B.AIR); // 咧开的颌骨
set(hx+2,hy+2,z-1,B.GLOW); set(hx+2,hy+2,z+1,B.GLOW); // 余烬之眼
},
wizard(c){ // 贤者之塔
const{x,z,gy,set,rng}=c;
c.fill(x-8,gy+1,z-8,x+8,gy+26,z+8,B.AIR);
c.fill(x-4,gy-4,z-4,x+4,gy,z+4,B.BRICK);
const H=16;
for(let h=1;h<=H;h++)c.disc(x,gy+h,z,4,B.BRICK,true); // 圆塔壁
c.disc(x,gy+H+1,z,5,B.SLATE); // 出挑檐
for(let k=0;k<=4;k++)c.disc(x,gy+H+2+k,z,4-k,B.SLATE); // 锥顶
set(x,gy+H+7,z,B.GLOW);
c.fill(x-1,gy+1,z-4,x+1,gy+3,z-4,B.AIR); // 塔门
for(let h=4;h<H;h+=4){set(x+4,gy+h,z,B.GLOW);set(x-4,gy+h,z,B.GLOW);}
for(let i=0;i<7;i++){ // 悬浮碎石
const a=rng()*Math.PI*2, rr=6+rng()*2.5, hh=gy+8+rng()*12;
set(Math.round(x+Math.cos(a)*rr),Math.round(hh),Math.round(z+Math.sin(a)*rr),B.STONE,true);
}
},
torii(c){ // 千本鸟居
const{x,z,gy,set}=c;
c.fill(x-6,gy+1,z-14,x+6,gy+10,z+14,B.AIR);
c.fill(x-2,gy-2,z-13,x+2,gy-1,z+13,B.DIRT);
c.fill(x-2,gy,z-13,x+2,gy,z+13,B.SANDSTONE); // 参道
for(let g=0;g<4;g++){
const gz=z-12+g*8;
for(const sg of[-1,1]){
const g0=heightAt(x+sg*3,gz);
for(let y=Math.max(1,g0-2);y<=gy;y++)set(x+sg*3,y,gz,B.STONE);
c.col(x+sg*3,gz,gy+1,gy+6,B.RED);
}
c.fill(x-3,gy+5,gz,x+3,gy+5,gz,B.RED); // 贯
c.fill(x-4,gy+7,gz,x+4,gy+7,gz,B.RED); // 笠木
c.fill(x-4,gy+8,gz,x+4,gy+8,gz,B.SLATE);
}
for(let g=0;g<3;g++){ // 石灯笼
const lz=z-8+g*8;
for(const sg of[-1,1]){
set(x+sg*5,gy+1,lz,B.STONE);
set(x+sg*5,gy+2,lz,B.GLOW);
set(x+sg*5,gy+3,lz,B.SLATE);
}
}
},
portal(c){ // 黑曜石遗门
const{x,z,gy,set,rng}=c;
c.fill(x-7,gy+1,z-5,x+7,gy+8,z+5,B.AIR);
c.fill(x-5,gy-3,z-2,x+5,gy,z+2,B.MOSSY);
for(let k=-2;k<=2;k++){
if(k!==1)set(x+k,gy+1,z,B.OBSIDIAN);
if(k!==-1&&k!==0)set(x+k,gy+6,z,B.OBSIDIAN);
}
for(let h=2;h<=5;h++){
set(x-2,gy+h,z,B.OBSIDIAN);
if(h<4)set(x+2,gy+h,z,B.OBSIDIAN); // 右上残破
}
for(let i=0;i<10;i++){ // 碎砾
const dx=Math.round((rng()-0.5)*10), dz=Math.round((rng()-0.5)*6);
set(x+dx,gy+1,z+dz,rng()<0.5?B.OBSIDIAN:B.MOSSY,true);
}
set(x,gy,z,B.GLOW); // 地底幽光
},
crystal(c){ // 水晶秘境
const{x,z,gy,set,rng}=c;
c.fill(x-9,gy+1,z-9,x+9,gy+16,z+9,B.AIR);
c.disc(x,gy-1,z,8,B.SLATE);
c.disc(x,gy,z,8,B.SLATE);
c.disc(x,gy,z,8,B.OBSIDIAN,true);
for(let i=0;i<8;i++){
const a=rng()*Math.PI*2, rr=rng()*6;
const px=x+Math.cos(a)*rr, pz=z+Math.sin(a)*rr;
const h=4+Math.floor(rng()*9);
const tx=(rng()-0.5)*0.35, tz=(rng()-0.5)*0.35;
for(let k=0;k<=h;k++){
const bx=Math.round(px+tx*k), bz=Math.round(pz+tz*k);
set(bx,gy+1+k,bz,B.CRYSTAL);
if(k<h*0.5){
set(bx+1,gy+1+k,bz,B.CRYSTAL,true); set(bx-1,gy+1+k,bz,B.CRYSTAL,true);
set(bx,gy+1+k,bz+1,B.CRYSTAL,true); set(bx,gy+1+k,bz-1,B.CRYSTAL,true);
}
}
}
},
skyshrine(c){ // 浮空圣殿(y 固定于云端)
const{x,z,gy,set,rng}=c;
const ty=gy, R=9;
for(let dz=-R;dz<=R;dz++)for(let dx=-R;dx<=R;dx++){ // 泪滴形浮岛
const d=Math.sqrt(dx*dx+dz*dz);
const edge=R-1.5+NZ.n2((x+dx)*0.3,(z+dz)*0.3)*1.8;
if(d>edge)continue;
const depth=Math.max(1,Math.round((1-d/Math.max(edge,0.01))*7+1));
set(x+dx,ty,z+dz,B.GRASS);
for(let k=1;k<=depth;k++)set(x+dx,ty-k,z+dz,k<=2?B.DIRT:B.STONE);
}
for(let i=0;i<6;i++){ // 环柱
const a=i/6*Math.PI*2;
c.col(Math.round(x+Math.cos(a)*4),Math.round(z+Math.sin(a)*4),ty+1,ty+4,B.PILLAR);
}
c.disc(x,ty+5,z,4,B.QUARTZ); // 顶环
set(x,ty+6,z,B.GOLD); set(x,ty+7,z,B.GLOW);
set(x,ty+1,z,B.SLATE); set(x,ty+2,z,B.CRYSTAL); // 圣坛
for(let i=0;i<5;i++){ // 悬垂水晶
const a=rng()*Math.PI*2, rr=2+rng()*4;
const px=Math.round(x+Math.cos(a)*rr), pz=Math.round(z+Math.sin(a)*rr);
const len=2+Math.floor(rng()*4);
for(let k=0;k<len;k++)set(px,ty-7-k,pz,B.CRYSTAL,true);
}
for(let i=0;i<8;i++){ // 神木藤蔓
const a=rng()*Math.PI*2;
const px=Math.round(x+Math.cos(a)*(R-2)), pz=Math.round(z+Math.sin(a)*(R-2));
const len=2+Math.floor(rng()*3);
for(let k=1;k<=len;k++)set(px,ty-k,pz,B.MYTHLEAF,true);
}
}
};
/* ============================================================
区块数据生成
============================================================ */
function genChunkData(cx,cz){
const data=new Uint8Array(CS*CH*CS);
const x0=cx*CS, z0=cz*CS;
for(let lz=0;lz<CS;lz++)for(let lx=0;lx<CS;lx++){
const wx=x0+lx, wz=z0+lz;
const h=heightAt(wx,wz);
const bio=biomeAt(wx,wz);
const beach=h<=SEA+1;
for(let y=0;y<CH;y++){
let id=B.AIR;
if(y===0)id=B.BEDROCK;
else if(y<=h){
if(y===h){
if(h>=63)id=B.SNOW;
else if(h>=57)id=B.STONE;
else if(bio===BIO.DESERT||beach)id=B.SAND;
else id=B.GRASS;
}else if(y>=h-3){
if(h>=57)id=B.STONE;
else if(bio===BIO.DESERT||beach)id=(y>=h-2)?B.SAND:B.SANDSTONE;
else id=B.DIRT;
}else{
id=B.STONE;
const hv=hash3(wx,y,wz);
if(y<34&&hv<0.012)id=B.GOLDORE;
else if(y<20&&hv>0.996)id=B.CRYSTAL;
}
if(id!==B.BEDROCK&&h>SEA+2&&y>4&&y<h-3&&caveAt(wx,y,wz))id=B.AIR;
}else if(y<=SEA){
id=B.WATER;
}
if(id!==B.AIR)data[(y*CS+lz)*CS+lx]=id;
}
const isl=skyIslandColumn(wx,wz);
if(isl&&isl.top>Math.max(h,SEA)+6){
for(let k=0;k<isl.depth;k++){
const y=isl.top-k;
if(y<1||y>=CH)continue;
data[(y*CS+lz)*CS+lx]=(k===0)?B.GRASS:(k<=2?B.DIRT:B.STONE);
}
const r=hash2(wx,wz);
if(isl.top+1<CH){
if(r<0.02)data[((isl.top+1)*CS+lz)*CS+lx]=B.CRYSTAL;
else if(r>0.99)data[((isl.top+1)*CS+lz)*CS+lx]=B.GLOW;
}
}
}
const set=makeSetter(data,cx,cz);
for(let dz=-3;dz<CS+3;dz++)for(let dx=-3;dx<CS+3;dx++){ // 树木(带边缘余量,跨区块一致)
const wx=x0+dx, wz=z0+dz;
if(biomeAt(wx,wz)!==BIO.FOREST)continue;
const h=heightAt(wx,wz);
if(h<=SEA+1||h>=56)continue;
if(hash2(wx,wz)>=0.035)continue;
stampTree(set,wx,h,wz,hash2(wx+999,wz-999));
}
for(const s of collectStructures(cx,cz)){ // 奇观(确定性播种)
BUILDERS[s.t](makeCtx(set,s));
}
return data;
}
/* ============================================================
区块缓存与世界方块访问
============================================================ */
const chunkMap=new Map();
const ck=(cx,cz)=>cx+','+cz;
function ensureData(cx,cz){ // 惰性生成(确定性,可随时重建)
const k=ck(cx,cz);
let c=chunkMap.get(k);
if(!c){c={cx,cz,data:genChunkData(cx,cz),meshes:null,meshed:false};chunkMap.set(k,c);}
return c;
}
function getBlockWorld(x,y,z){
if(y<0)return B.BEDROCK;
if(y>=CH)return B.AIR;
return ensureData(x>>4,z>>4).data[(y*CS+(z&15))*CS+(x&15)];
}
function setBlockWorld(x,y,z,id){
if(y<1||y>=CH)return false; // 最底层基岩不可改写
ensureData(x>>4,z>>4).data[(y*CS+(z&15))*CS+(x&15)]=id;
return true;
}
/* ============================================================
面表 / 环境光遮蔽 / 区块网格化
============================================================ */
// 面顺序:-x,+x,-y,+y,-z,+z;c=[x,y,z,u,v]×4;d=法向;sh=面基础明暗
// t1/t2 = 面内两条切向轴(0=x,1=y,2=z),用于 AO 采样
const FACES=[
{d:[-1,0,0],t1:1,t2:2,sh:0.76,c:[[0,1,0,0,1],[0,0,0,0,0],[0,1,1,1,1],[0,0,1,1,0]]},
{d:[ 1,0,0],t1:1,t2:2,sh:0.76,c:[[1,1,1,0,1],[1,0,1,0,0],[1,1,0,1,1],[1,0,0,1,0]]},
{d:[0,-1,0],t1:0,t2:2,sh:0.50,c:[[1,0,1,1,0],[0,0,1,0,0],[1,0,0,1,1],[0,0,0,0,1]]},
{d:[0, 1,0],t1:0,t2:2,sh:1.00,c:[[0,1,1,1,1],[1,1,1,0,1],[0,1,0,1,0],[1,1,0,0,0]]},
{d:[0,0,-1],t1:0,t2:1,sh:0.62,c:[[1,0,0,0,0],[0,0,0,1,0],[1,1,0,0,1],[0,1,0,1,1]]},
{d:[0,0, 1],t1:0,t2:1,sh:0.62,c:[[0,0,1,0,0],[1,0,1,1,0],[0,1,1,0,1],[1,1,1,1,1]]}
];
const AOL=[0.42,0.62,0.80,1.00]; // AO 等级 0..3 → 亮度
// 构建一个区块的几何数组:sol=不透明 glo=发光(夜间不变暗) wat=水
function buildChunkArrays(cx,cz){
for(let dz=-1;dz<=1;dz++)for(let dx=-1;dx<=1;dx++)ensureData(cx+dx,cz+dz); // 先备齐 3×3 邻居,跨界剔除/AO 才正确
const data=ensureData(cx,cz).data;
const x0=cx*CS, z0=cz*CS;
const sol={pos:[],uv:[],col:[],idx:[]};
const glo={pos:[],uv:[],col:[],idx:[]};
const wat={pos:[],uv:[],col:[],idx:[]};
const gb=(x,y,z)=>{ // 局部坐标读方块(可越界 ±1)
if(y<0)return B.BEDROCK;
if(y>=CH)return B.AIR;
if(x>=0&&x<CS&&z>=0&&z<CS)return data[(y*CS+z)*CS+x];
return getBlockWorld(x0+x,y,z0+z);
};
const solidAt=(x,y,z)=>{const i=gb(x,y,z);return i!==B.AIR&&i!==B.WATER;};
const uvPush=(arr,tile,ul,vl)=>{arr.push((tile%8+ul)/8, 1-(((tile/8)|0)+(1-vl))/4);};
function emit(tg,x,y,z,f,tile,noAO){
const fa=FACES[f], base=tg.pos.length/3, aos=[3,3,3,3];
for(let i=0;i<4;i++){
const cr=fa.c[i];
tg.pos.push(x+cr[0],y+cr[1],z+cr[2]);
uvPush(tg.uv,tile,cr[3],cr[4]);
if(!noAO){ // 经典 0fps AO:side1/side2/corner
const nx=x+fa.d[0],ny=y+fa.d[1],nz=z+fa.d[2];
const o1=[0,0,0],o2=[0,0,0];
o1[fa.t1]=cr[fa.t1]?1:-1;
o2[fa.t2]=cr[fa.t2]?1:-1;
const s1=solidAt(nx+o1[0],ny+o1[1],nz+o1[2])?1:0;
const s2=solidAt(nx+o2[0],ny+o2[1],nz+o2[2])?1:0;
const sc=solidAt(nx+o1[0]+o2[0],ny+o1[1]+o2[1],nz+o1[2]+o2[2])?1:0;
aos[i]=(s1&&s2)?0:3-(s1+s2+sc);
}
const l=fa.sh*AOL[aos[i]];
tg.col.push(l,l,l);
}
if(aos[0]+aos[3]>aos[1]+aos[2]) // 翻转对角线,消除 AO 各向异性
tg.idx.push(base,base+1,base+3, base,base+3,base+2);
else tg.idx.push(base,base+1,base+2, base+2,base+1,base+3);
}
for(let y=0;y<CH;y++)for(let lz=0;lz<CS;lz++)for(let lx=0;lx<CS;lx++){
const id=data[(y*CS+lz)*CS+lx];
if(id===B.AIR)continue;
const def=BLOCKS[id];
if(def.water){ // 水:仅对空气出面,水面下沉 0.12
const capped=gb(lx,y+1,lz)===B.WATER;
for(let f=0;f<6;f++){
const fa=FACES[f];
if(gb(lx+fa.d[0],y+fa.d[1],lz+fa.d[2])!==B.AIR)continue;
const base=wat.pos.length/3;
const lum=f===3?0.95:(f===2?0.62:0.80);
for(let i=0;i<4;i++){
const cr=fa.c[i];
const yy=(cr[1]===1&&!capped)?y+0.88:y+cr[1]; // 上方还是水则连成整体
wat.pos.push(lx+cr[0],yy,lz+cr[2]);
uvPush(wat.uv,T.water,cr[3],cr[4]);
wat.col.push(lum,lum,lum);
}
wat.idx.push(base,base+1,base+2, base+2,base+1,base+3);
}
continue;
}
const tg=def.glow?glo:sol;
for(let f=0;f<6;f++){
const fa=FACES[f];
const nid=gb(lx+fa.d[0],y+fa.d[1],lz+fa.d[2]);
if(nid!==B.AIR&&nid!==B.WATER)continue; // 被实体遮挡则剔除
const tile=f===3?def.t:(f===2?def.b:def.s);
emit(tg,lx,y,lz,f,tile,!!def.glow); // 发光方块跳过 AO,保持满亮
}
}
return {sol,glo,wat};
}
/* ============================================================
体素射线投射(Amanatides & Woo DDA)
============================================================ */
// 返回命中方块的世界坐标 + 进入面法向(放置用),未命中返回 null
function raycastVoxel(ox,oy,oz,dx,dy,dz,maxDist){
let x=Math.floor(ox),y=Math.floor(oy),z=Math.floor(oz);
const stx=dx>0?1:-1, sty=dy>0?1:-1, stz=dz>0?1:-1;
const tdx=dx!==0?Math.abs(1/dx):Infinity;
const tdy=dy!==0?Math.abs(1/dy):Infinity;
const tdz=dz!==0?Math.abs(1/dz):Infinity;
let tmx=dx!==0?(dx>0?(x+1-ox):(ox-x))*tdx:Infinity;
let tmy=dy!==0?(dy>0?(y+1-oy):(oy-y))*tdy:Infinity;
let tmz=dz!==0?(dz>0?(z+1-oz):(oz-z))*tdz:Infinity;
let t=0,nx=0,ny=0,nz=0;
for(let i=0;i<256;i++){ // 先步进后检查:跳过起点所在格
if(tmx<tmy&&tmx<tmz){x+=stx;t=tmx;tmx+=tdx;nx=-stx;ny=0;nz=0;}
else if(tmy<=tmz) {y+=sty;t=tmy;tmy+=tdy;nx=0;ny=-sty;nz=0;}
else {z+=stz;t=tmz;tmz+=tdz;nx=0;ny=0;nz=-stz;}
if(t>maxDist)return null;
const id=getBlockWorld(x,y,z);
if(id!==B.AIR&&id!==B.WATER)return{x,y,z,id,nx,ny,nz};
}
return null;
}
// ===== CORE END =====
/* ============================================================
RUNTIME · 程序化像素纹理图集(8×4 tile,每格 16px)
============================================================ */
const TILE=16, ACOLS=8;
const atlas=document.createElement('canvas');
atlas.width=ACOLS*TILE; atlas.height=4*TILE;
const actx=atlas.getContext('2d');
function paintTile(t,fn){
const ox=(t%ACOLS)*TILE, oy=((t/ACOLS)|0)*TILE;
const rng=mulberry32(1000+t);
const hsl=(h,s,l)=>'hsl('+h+','+s+'%,'+l+'%)';
const px=(x,y,c)=>{actx.fillStyle=c;actx.fillRect(ox+x,oy+y,1,1);};
const rect=(x,y,w,h,c)=>{actx.fillStyle=c;actx.fillRect(ox+x,oy+y,w,h);};
const noise=(h,s,l,jit)=>{for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++)px(x,y,hsl(h+(rng()-0.5)*8,s,l+(rng()-0.5)*jit));};
const speck=(n,h,s,l,sz)=>{for(let i=0;i<n;i++)rect((rng()*(TILE-1))|0,(rng()*(TILE-1))|0,sz||1,sz||1,hsl(h,s,l));};
fn({px,rect,noise,speck,rng,hsl});
}
function buildAtlas(){
paintTile(T.grassTop ,a=>{a.noise(105,45,38,12);a.speck(10,108,50,30);a.speck(6,100,42,48);});
paintTile(T.grassSide,a=>{a.noise(28,38,34,10);a.speck(8,26,40,26);
for(let x=0;x<TILE;x++){const d=2+((a.rng()*3)|0);for(let y=0;y<d;y++)a.px(x,y,a.hsl(105+(a.rng()-0.5)*10,46,36+(a.rng()-0.5)*10));}});
paintTile(T.dirt ,a=>{a.noise(28,38,34,10);a.speck(9,26,42,24);a.speck(5,30,35,44);});
paintTile(T.stone ,a=>{a.noise(0,0,48,9);a.speck(6,0,0,40,2);a.speck(4,0,0,56,2);});
paintTile(T.sand ,a=>{a.noise(48,52,72,7);a.speck(8,46,55,63);});
paintTile(T.logSide ,a=>{for(let x=0;x<TILE;x++){const base=26+(a.rng()-0.5)*9;
for(let y=0;y<TILE;y++)a.px(x,y,a.hsl(25,42,base+(a.rng()-0.5)*5));}
for(let i=0;i<4;i++){const x=(a.rng()*TILE)|0;a.rect(x,(a.rng()*10)|0,1,3+(a.rng()*4|0),a.hsl(25,40,18));}});
paintTile(T.logTop ,a=>{a.noise(25,42,24,6);
for(let y=2;y<14;y++)for(let x=2;x<14;x++){const r=Math.max(Math.abs(x-7.5),Math.abs(y-7.5))|0;
a.px(x,y,a.hsl(33,40,(r%2?46:38)+(a.rng()-0.5)*5));}});
paintTile(T.leaf ,a=>{a.noise(110,45,30,14);a.speck(12,112,48,16);a.speck(6,108,46,42);});
paintTile(T.mythLeaf ,a=>{a.noise(165,55,44,14);a.speck(10,168,60,28);a.speck(7,160,30,90);});
paintTile(T.snow ,a=>{a.noise(0,0,93,4);a.speck(5,210,20,86);});
paintTile(T.water ,a=>{a.noise(208,70,48,8);
for(let i=0;i<5;i++){const y=(a.rng()*TILE)|0,x=(a.rng()*10)|0;a.rect(x,y,3+(a.rng()*4|0),1,a.hsl(204,72,60));}});
paintTile(T.quartz ,a=>{a.noise(40,18,88,4);
for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++)if((x+y)%6===0)a.px(x,y,a.hsl(40,16,83));});
paintTile(T.pillarSide,a=>{for(let x=0;x<TILE;x++){const m=x%4,l=m===0?72:(m===2?92:86);
for(let y=0;y<TILE;y++)a.px(x,y,a.hsl(42,18,l+(a.rng()-0.5)*4));}
a.rect(0,0,TILE,2,a.hsl(43,25,80));a.rect(0,14,TILE,2,a.hsl(43,25,78));a.rect(0,2,TILE,1,a.hsl(40,18,66));a.rect(0,13,TILE,1,a.hsl(40,18,66));});
paintTile(T.pillarTop,a=>{a.noise(40,18,88,4);
for(let i=0;i<TILE;i++){a.px(i,0,a.hsl(40,18,70));a.px(i,15,a.hsl(40,18,70));a.px(0,i,a.hsl(40,18,70));a.px(15,i,a.hsl(40,18,70));}
for(let i=2;i<14;i++){a.px(i,2,a.hsl(40,18,78));a.px(i,13,a.hsl(40,18,78));a.px(2,i,a.hsl(40,18,78));a.px(13,i,a.hsl(40,18,78));}});
paintTile(T.gold ,a=>{a.noise(46,85,55,8);a.speck(5,48,95,75,2);a.speck(4,44,80,42);});
paintTile(T.goldOre ,a=>{a.noise(0,0,48,9);
for(let i=0;i<4;i++){const x=1+(a.rng()*12)|0,y=1+(a.rng()*12)|0;a.rect(x,y,2,2,a.hsl(47,90,58));a.px(x,y,a.hsl(50,95,72));}});
paintTile(T.brick ,a=>{a.noise(0,0,52,6);
for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){const row=(y/4)|0,off=(row%2)*4;
if(y%4===3||((x+off)%8===0))a.px(x,y,a.hsl(0,0,38));}});
paintTile(T.mossy ,a=>{a.noise(0,0,52,6);
for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){const row=(y/4)|0,off=(row%2)*4;
if(y%4===3||((x+off)%8===0))a.px(x,y,a.hsl(0,0,38));}
a.speck(14,110,40,32,2);a.speck(6,112,45,40);});
paintTile(T.sandstone,a=>{a.noise(45,40,68,5);
for(let y=0;y<TILE;y+=5)for(let x=0;x<TILE;x++)a.px(x,y,a.hsl(44,38,61));});
paintTile(T.obsidian ,a=>{a.noise(265,30,10,5);a.speck(8,275,60,28);a.speck(4,280,50,18,2);});
paintTile(T.glow ,a=>{a.noise(42,95,60,12);a.speck(8,45,100,78,2);a.speck(5,40,90,50);});
paintTile(T.crystal ,a=>{a.noise(174,80,62,10);
for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++)if((x+y)%5===0)a.px(x,y,a.hsl(172,85,82));});
paintTile(T.bone ,a=>{a.noise(45,15,82,5);
for(let i=0;i<3;i++){const x=2+(a.rng()*12)|0,y=(a.rng()*8)|0;a.rect(x,y,1,3+(a.rng()*4|0),a.hsl(42,14,64));}});
paintTile(T.red ,a=>{for(let x=0;x<TILE;x++){const base=40+(a.rng()-0.5)*10;
for(let y=0;y<TILE;y++)a.px(x,y,a.hsl(8,68,base+(a.rng()-0.5)*5));}
for(let i=0;i<3;i++)a.rect((a.rng()*TILE)|0,(a.rng()*10)|0,1,4,a.hsl(6,65,26));});
paintTile(T.slate ,a=>{a.noise(225,12,22,6);a.speck(6,225,14,32);});
paintTile(T.plank ,a=>{for(let y=0;y<TILE;y++){const band=((y/4)|0)%2,l=46+band*5;
for(let x=0;x<TILE;x++)a.px(x,y,a.hsl(30,42,l+(a.rng()-0.5)*5));}
for(let y=3;y<TILE;y+=4)a.rect(0,y,TILE,1,a.hsl(28,40,32));
for(let r=0;r<4;r++)a.px(3+((r*5)%13),r*4+1,a.hsl(28,40,30));});
paintTile(T.bedrock ,a=>{a.noise(0,0,30,26);a.speck(6,0,0,12,2);});
}
/* ============================================================
THREE 场景 / 材质 / 天空
============================================================ */
let renderer,camera,scene,tex,matSolid,matGlow,matWater,sun,moon,clouds=[],hl;
let RD=5;
const SKY_DAY=0x8ec8f0, SKY_SET=0xe7935c, SKY_NIGHT=0x0b1126;
function setupThree(){
renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,1.5));
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.domElement.id='game';
document.body.insertBefore(renderer.domElement,document.body.firstChild);
scene=new THREE.Scene();
camera=new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000);
camera.rotation.order='YXZ';
scene.fog=new THREE.Fog(SKY_DAY,40,RD*16*0.95);
tex=new THREE.CanvasTexture(atlas);
tex.magFilter=THREE.NearestFilter; tex.minFilter=THREE.NearestFilter; tex.generateMipmaps=false;
matSolid=new THREE.MeshBasicMaterial({map:tex,vertexColors:true});
matGlow =new THREE.MeshBasicMaterial({map:tex,vertexColors:true});
matWater=new THREE.MeshBasicMaterial({map:tex,vertexColors:true,transparent:true,opacity:0.72,side:THREE.DoubleSide,depthWrite:false});
sun=new THREE.Mesh(new THREE.CircleGeometry(22,20),new THREE.MeshBasicMaterial({color:0xffe9a8,fog:false}));
moon=new THREE.Mesh(new THREE.CircleGeometry(15,20),new THREE.MeshBasicMaterial({color:0xdfe8ff,fog:false}));
scene.add(sun);scene.add(moon);
const cmat=new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:0.5});
const crng=mulberry32(SEED+99);
for(let i=0;i<14;i++){
const c=new THREE.Mesh(new THREE.BoxGeometry(18+crng()*22,2,12+crng()*14),cmat);
c.userData={ox:(crng()-0.5)*480,oz:(crng()-0.5)*480,sp:1.2+crng()*1.4};
c.position.y=86+crng()*7;
scene.add(c);clouds.push(c);
}
hl=new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(1.002,1.002,1.002)),
new THREE.LineBasicMaterial({color:0x000000}));
hl.visible=false;scene.add(hl);
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
}
function updateFog(){scene.fog.far=RD*16*0.95;scene.fog.near=Math.min(40,scene.fog.far*0.45);}
/* ============================================================
区块网格管理
============================================================ */
function disposeMeshes(c){
if(c.meshes){for(const m of c.meshes){scene.remove(m);m.geometry.dispose();}}
c.meshes=null;c.meshed=false;
}
function buildChunkMeshes(cx,cz){
const c=ensureData(cx,cz);
if(c.meshes)disposeMeshes(c);
const arr=buildChunkArrays(cx,cz);
const meshes=[];
const mk=(g,mat,ro)=>{
if(!g.pos.length)return;
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.Float32BufferAttribute(g.pos,3));
geo.setAttribute('uv',new THREE.Float32BufferAttribute(g.uv,2));
geo.setAttribute('color',new THREE.Float32BufferAttribute(g.col,3));
geo.setIndex(g.idx);
const m=new THREE.Mesh(geo,mat);
m.position.set(cx*CS,0,cz*CS);
m.matrixAutoUpdate=false;m.updateMatrix();
if(ro)m.renderOrder=ro;
scene.add(m);meshes.push(m);
};
mk(arr.sol,matSolid,0); mk(arr.glo,matGlow,1); mk(arr.wat,matWater,2);
c.meshes=meshes;c.meshed=true;
}
// 编辑后重建:本块 + 紧贴边界的邻块(含角落)
function rebuildAt(wx,wz){
const cx=wx>>4,cz=wz>>4,lx=wx&15,lz=wz&15;
const set=new Set([ck(cx,cz)]);
if(lx===0)set.add(ck(cx-1,cz)); if(lx===15)set.add(ck(cx+1,cz));
if(lz===0)set.add(ck(cx,cz-1)); if(lz===15)set.add(ck(cx,cz+1));
if(lx===0&&lz===0)set.add(ck(cx-1,cz-1)); if(lx===15&&lz===0)set.add(ck(cx+1,cz-1));
if(lx===0&&lz===15)set.add(ck(cx-1,cz+1)); if(lx===15&&lz===15)set.add(ck(cx+1,cz+1));
for(const k of set){
const c=chunkMap.get(k);
if(c&&c.meshed){const p=k.split(',');buildChunkMeshes(+p[0],+p[1]);}
}
}
// 由近及远的偏移表
const OFFS=[];
for(let dz=-8;dz<=8;dz++)for(let dx=-8;dx<=8;dx++)OFFS.push([dx,dz,dx*dx+dz*dz]);
OFFS.sort((a,b)=>a[2]-b[2]);
let frameNo=0;
function chunkTick(pcx,pcz,budget){
let built=0;
for(const o of OFFS){
if(o[2]>RD*RD)break;
const cx=pcx+o[0],cz=pcz+o[1];
const c=chunkMap.get(ck(cx,cz));
if(!c||!c.meshed){buildChunkMeshes(cx,cz);if(++built>=budget)break;}
}
if(frameNo%120===60){ // 周期性卸载远处网格(保留数据)
for(const c of chunkMap.values()){
if(c.meshed&&Math.max(Math.abs(c.cx-pcx),Math.abs(c.cz-pcz))>RD+2)disposeMeshes(c);
}
}
}
/* ============================================================
音效(WebAudio 合成,无外部资源)
============================================================ */
let AC=null;
function sfx(kind){
try{
AC=AC||new (window.AudioContext||window.webkitAudioContext)();
if(AC.state==='suspended')AC.resume();
const o=AC.createOscillator(),g=AC.createGain(),t=AC.currentTime;
o.connect(g);g.connect(AC.destination);
if(kind==='break'){o.type='square';o.frequency.setValueAtTime(170,t);o.frequency.exponentialRampToValueAtTime(50,t+0.12);g.gain.setValueAtTime(0.11,t);}
else{o.type='triangle';o.frequency.setValueAtTime(330,t);o.frequency.exponentialRampToValueAtTime(190,t+0.09);g.gain.setValueAtTime(0.09,t);}
g.gain.exponentialRampToValueAtTime(0.001,t+0.14);
o.start(t);o.stop(t+0.15);
}catch(e){}
}
/* ============================================================
玩家与物理(AABB 0.6×1.8,眼高 1.62)
============================================================ */
const player={x:2.5,y:40,z:26.5,vx:0,vy:0,vz:0,yaw:-0.72,pitch:-0.06,fly:false,onGround:false,sel:0,sprint:false};
function respawn(){player.x=2.5;player.z=26.5;player.y=heightAt(2,26)+3;player.vx=player.vy=player.vz=0;}
function solidWorld(x,y,z){const id=getBlockWorld(x,y,z);return id!==B.AIR&&id!==B.WATER;}
function boxHits(px,py,pz){
const e=1e-4;
const x0=Math.floor(px-0.3+e),x1=Math.floor(px+0.3-e);
const y0=Math.floor(py+e), y1=Math.floor(py+1.8-e);
const z0=Math.floor(pz-0.3+e),z1=Math.floor(pz+0.3-e);
for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++)for(let x=x0;x<=x1;x++)
if(solidWorld(x,y,z))return true;
return false;
}
function moveAxis(ax,amt){ // 逐轴推进,碰撞则小步贴墙
if(!amt)return;
player[ax]+=amt;
if(!boxHits(player.x,player.y,player.z))return;
player[ax]-=amt;
const st=Math.sign(amt)*0.04;let moved=0;
while(Math.abs(moved)+0.04<=Math.abs(amt)){
player[ax]+=st;
if(boxHits(player.x,player.y,player.z)){player[ax]-=st;break;}
moved+=st;
}
if(ax==='y'){if(amt<0)player.onGround=true;player.vy=0;}
else if(ax==='x')player.vx=0;else player.vz=0;
}
const bodyInWater=()=>getBlockWorld(Math.floor(player.x),Math.floor(player.y+0.6),Math.floor(player.z))===B.WATER;
function updatePlayer(dt){
const inW=bodyInWater();
let f=0,s=0;
if(keys.has('KeyW'))f+=1; if(keys.has('KeyS'))f-=1;
if(keys.has('KeyD'))s+=1; if(keys.has('KeyA'))s-=1;
player.sprint=(keys.has('ControlLeft')||keys.has('ControlRight'))&&f>0&&!inW;
const sp=player.fly?(player.sprint?14:9):(inW?3.2:(player.sprint?7.0:4.5));
const fx=-Math.sin(player.yaw),fz=-Math.cos(player.yaw);
const rx= Math.cos(player.yaw),rz=-Math.sin(player.yaw);
if(f||s){
let mx=fx*f+rx*s, mz=fz*f+rz*s;
const ml=Math.hypot(mx,mz)||1;
player.vx=mx/ml*sp; player.vz=mz/ml*sp;
}else{
const dmp=Math.pow(0.0001,dt);
player.vx*=dmp;player.vz*=dmp;
if(Math.abs(player.vx)<0.01)player.vx=0;
if(Math.abs(player.vz)<0.01)player.vz=0;
}
if(player.fly){
let ty=0;
if(keys.has('Space'))ty=9;
if(keys.has('ShiftLeft')||keys.has('ShiftRight'))ty=-9;
player.vy=lerp(player.vy,ty,Math.min(1,dt*10));
}else if(inW){
player.vy+=-26*0.3*dt;
if(keys.has('Space'))player.vy=4; // 浮力划水
player.vy*=Math.pow(0.4,dt);
player.vy=clamp(player.vy,-6,6);
}else{
player.vy-=26*dt;
if(player.vy<-50)player.vy=-50;
if(keys.has('Space')&&player.onGround){player.vy=8.2;player.onGround=false;}
}
player.onGround=false;
moveAxis('x',player.vx*dt);
moveAxis('z',player.vz*dt);
moveAxis('y',player.vy*dt);
if(player.y<-30){respawn();toast(S('respawnTitle'),S('respawnBody'),2.2);}
}
/* ============================================================
输入:键盘 / 指针锁定(含拖拽回退)/ 鼠标交互
============================================================ */
const keys=new Set();
let state='boot',usingFallback=false,lockWanted=false;
let holdTimer=null,dragInfo=null;
function camDir(){
const cp=Math.cos(player.pitch);
return [-Math.sin(player.yaw)*cp, Math.sin(player.pitch), -Math.cos(player.yaw)*cp];
}
function aim(){
const d=camDir();
return raycastVoxel(camera.position.x,camera.position.y,camera.position.z,d[0],d[1],d[2],7);
}
function doBreak(){
const h=aim();if(!h)return;
if(h.id===B.BEDROCK){toast(S('bedrockTitle'),S('bedrockBody'),1.4);return;}
setBlockWorld(h.x,h.y,h.z,B.AIR);sfx('break');rebuildAt(h.x,h.z);
}
function doPlace(){
const h=aim();if(!h)return;
const tx=h.x+h.nx,ty=h.y+h.ny,tz=h.z+h.nz;
const cur=getBlockWorld(tx,ty,tz);
if(cur!==B.AIR&&cur!==B.WATER)return;
if(!(tx+1<=player.x-0.3||tx>=player.x+0.3||ty+1<=player.y||ty>=player.y+1.8||tz+1<=player.z-0.3||tz>=player.z+0.3))return; // 不能放进自己身体
if(setBlockWorld(tx,ty,tz,HOTBAR[player.sel])){sfx('place');rebuildAt(tx,tz);}
}
function startHold(btn){
stopHold();
if(btn===0)doBreak();else doPlace();
holdTimer=setInterval(()=>{btn===0?doBreak():doPlace();},240);
}
function stopHold(){if(holdTimer){clearInterval(holdTimer);holdTimer=null;}}
window.addEventListener('keydown',e=>{
if(state!=='playing'){
if((state==='ready'||state==='paused')&&(e.code==='Enter'))startGame();
return;
}
keys.add(e.code);
if(e.code==='Space')e.preventDefault();
if(e.code.indexOf('Digit')===0){const n=+e.code.slice(5);if(n>=1&&n<=9)selectSlot(n-1);}
else if(e.code==='KeyF'){player.fly=!player.fly;player.vy=0;toast(player.fly?S('flyOn'):S('flyOff'),player.fly?S('flyHint'):'',1.6);}
else if(e.code==='KeyG')teleportNext();
else if(e.code==='BracketLeft'){RD=Math.max(3,RD-1);updateFog();toast(S('renderDist')+RD+S('renderDistUnit'),'',1.2);}
else if(e.code==='BracketRight'){RD=Math.min(7,RD+1);updateFog();toast(S('renderDist')+RD+S('renderDistUnit'),'',1.2);}
else if(e.code==='Escape'&&usingFallback)pauseGame();
});
window.addEventListener('keyup',e=>keys.delete(e.code));
window.addEventListener('blur',()=>{keys.clear();stopHold();});
window.addEventListener('contextmenu',e=>{if(state==='playing')e.preventDefault();});
window.addEventListener('mousedown',e=>{
if(state!=='playing')return;
if(document.pointerLockElement)startHold(e.button===2?2:0);
else if(usingFallback){
if(e.button===0)dragInfo={x:e.clientX,y:e.clientY,moved:false};
else if(e.button===2)doPlace();
}
});
window.addEventListener('mousemove',e=>{
if(state!=='playing')return;
if(document.pointerLockElement){
player.yaw-=e.movementX*0.0024;
player.pitch=clamp(player.pitch-e.movementY*0.0024,-1.55,1.55);
}else if(usingFallback&&dragInfo){
const dx=e.clientX-dragInfo.x,dy=e.clientY-dragInfo.y;
if(Math.abs(dx)+Math.abs(dy)>5)dragInfo.moved=true;
player.yaw-=dx*0.006;
player.pitch=clamp(player.pitch-dy*0.006,-1.55,1.55);
dragInfo.x=e.clientX;dragInfo.y=e.clientY;
}
});
window.addEventListener('mouseup',e=>{
stopHold();
if(usingFallback&&dragInfo&&e.button===0){if(!dragInfo.moved)doBreak();dragInfo=null;}
});
window.addEventListener('wheel',e=>{
if(state!=='playing')return;
selectSlot((player.sel+(e.deltaY>0?1:-1)+9)%9);
},{passive:true});
document.addEventListener('pointerlockchange',()=>{
if(document.pointerLockElement){usingFallback=false;}
else if(state==='playing'&&!usingFallback)pauseGame();
});
function tryLock(){
lockWanted=true;
const el=renderer.domElement;
try{if(el.requestPointerLock)el.requestPointerLock();}catch(e){}
setTimeout(()=>{
if(lockWanted&&!document.pointerLockElement&&state==='playing'){
usingFallback=true;
toast(S('dragTitle'),S('dragBody'),3.2);
}
},350);
}
/* ============================================================
传送 / 提示 / HUD
============================================================ */
let wIdx=-1;const discovered=new Set();
function teleportNext(){
wIdx=(wIdx+1)%WONDERS.length;
const w=WONDERS[wIdx];
player.x=w.x+16.5;player.z=w.z+16.5;
player.y=(w.y!=null?w.y:heightAt(w.x,w.z))+16;
player.vx=player.vy=player.vz=0;player.fly=true;
player.yaw=Math.atan2(-(w.x-player.x),-(w.z-player.z));
player.pitch=-0.34;
discovered.add(w.t);
toast('✦ '+wName(w),wLore(w)+S('flyEnabled'),3.6);
}
let toastTimer=null;
function toast(title,body,sec){
const t=document.getElementById('toast');
t.querySelector('b').textContent=title;
const sp=t.querySelector('span');
sp.textContent=body||'';sp.style.display=body?'block':'none';
t.style.opacity='1';
if(toastTimer)clearTimeout(toastTimer);
toastTimer=setTimeout(()=>{t.style.opacity='0';},(sec||2.5)*1000);
}
function selectSlot(i){
player.sel=i;
const slots=document.querySelectorAll('#hotbar .slot');
for(let j=0;j<slots.length;j++)slots[j].classList.toggle('sel',j===i);
}
function buildHotbar(){
const bar=document.getElementById('hotbar');
for(let i=0;i<HOTBAR.length;i++){
const id=HOTBAR[i],def=BLOCKS[id];
const slot=document.createElement('div');slot.className='slot'+(i===0?' sel':'');
const c=document.createElement('canvas');c.width=32;c.height=32;
const g=c.getContext('2d');g.imageSmoothingEnabled=false;
const tile=(id===B.GRASS)?T.grassSide:def.t;
g.drawImage(atlas,(tile%ACOLS)*TILE,((tile/ACOLS)|0)*TILE,TILE,TILE,0,0,32,32);
const num=document.createElement('i');num.textContent=i+1;
const nm=document.createElement('u');nm.dataset.block=String(id);nm.textContent=bName(id);
slot.appendChild(c);slot.appendChild(num);slot.appendChild(nm);
bar.appendChild(slot);
}
}
function updateHotbarNames(){
document.querySelectorAll('#hotbar .slot u').forEach(el=>{
const id=+el.dataset.block;
if(BLOCKS[id])el.textContent=bName(id);
});
}
function fillWonders(){
let html='';
for(const o of WONDERS)html+='<div class="w"><b>'+wName(o)+'</b><span>'+wLore(o)+'</span></div>';
document.getElementById('wonders').innerHTML=html;
}
let fpsAvg=60;
const ARROWS='↑↗→↘↓↙←↖';
function updateHUD(dt){
fpsAvg=lerp(fpsAvg,1/Math.max(dt,1e-3),0.06);
if(frameNo%10===0){
let best=null,bd=1e9;
for(const w of WONDERS){const d=Math.hypot(w.x-player.x,w.z-player.z);if(d<bd){bd=d;best=w;}}
let comp='';
if(best){
const wy=Math.atan2(-(best.x-player.x),-(best.z-player.z));
let rel=wy-player.yaw;
rel=((rel%(Math.PI*2))+Math.PI*3)%(Math.PI*2)-Math.PI;
const ai=((Math.round(-rel/(Math.PI/4)))%8+8)%8;
comp=S('nearest')+wName(best)+' · '+(bd|0)+S('distUnit')+ARROWS.charAt(ai);
}
document.getElementById('info').textContent=
S('hudFps')+(fpsAvg|0)+' · XYZ '+Math.floor(player.x)+' '+Math.floor(player.y)+' '+Math.floor(player.z)+
'\n'+S('hudDist')+RD+' · '+S('hudHold')+bName(HOTBAR[player.sel])+(player.fly?S('hudFlying'):'')+comp;
}
if(frameNo%30===5){
for(const w of WONDERS){
if(discovered.has(w.t))continue;
if(Math.hypot(w.x-player.x,w.z-player.z)<30){
discovered.add(w.t);
toast(S('discover')+wName(w),wLore(w),4.2);
break;
}
}
}
}
function updateHL(){
const h=(state==='playing')?aim():null;
if(h){hl.position.set(h.x+0.5,h.y+0.5,h.z+0.5);hl.visible=true;}
else hl.visible=false;
}
/* ============================================================
昼夜循环 / 天空
============================================================ */
const DAY_LEN=240, TOD0=0.32;
let elapsed=0;
const cDay=new THREE.Color(SKY_DAY),cSet=new THREE.Color(SKY_SET),cNight=new THREE.Color(SKY_NIGHT);
const skyCol=new THREE.Color();
function updateSky(dt){
elapsed+=dt;
const tod=(TOD0+elapsed/DAY_LEN)%1;
const ang=tod*Math.PI*2-Math.PI/2;
const el=Math.sin(ang);
const b=clamp(0.34+0.66*smoothstep(-0.18,0.25,el),0.34,1); // 夜间整体压暗(发光方块除外)
matSolid.color.setScalar(b);
matWater.color.setScalar(b);
if(el>0.25)skyCol.copy(cDay);
else if(el>-0.05)skyCol.copy(cSet).lerp(cDay,(el+0.05)/0.30);
else skyCol.copy(cNight).lerp(cSet,clamp((el+0.45)/0.40,0,1));
scene.fog.color.copy(skyCol);
renderer.setClearColor(skyCol);
const R=380;
sun.position.set(player.x+Math.cos(ang)*R, player.y+Math.sin(ang)*R, player.z-110);
moon.position.set(player.x-Math.cos(ang)*R, player.y-Math.sin(ang)*R, player.z+110);
sun.lookAt(camera.position);moon.lookAt(camera.position);
for(const c of clouds){
const rel=((c.userData.ox+elapsed*c.userData.sp+240)%480+480)%480-240;
c.position.x=player.x+rel;
c.position.z=player.z+c.userData.oz;
}
}
/* ============================================================
开始 / 暂停 / 加载流程 / 主循环
============================================================ */
function startGame(){
document.getElementById('overlay').style.display='none';
document.getElementById('langswitch').style.display='none';
document.getElementById('hud').style.display='block';
state='playing';
if(AC&&AC.state==='suspended')AC.resume();
tryLock();
}
function pauseGame(){
if(state!=='playing')return;
state='paused';lockWanted=false;
keys.clear();stopHold();dragInfo=null;
const b=document.getElementById('startbtn');
b.textContent=S('continue');b.disabled=false;
document.getElementById('loadtext').textContent=S('paused');
document.getElementById('overlay').style.display='flex';
document.getElementById('langswitch').style.display='flex';
}
document.getElementById('startbtn').addEventListener('click',()=>{
if(state==='ready'||state==='paused')startGame();
});
let loadList=[],loadIdx=0,last=0;
function finishLoading(){
state='ready';
document.getElementById('loadfill').style.width='100%';
document.getElementById('loadtext').textContent=S('ready');
document.getElementById('startbtn').textContent=S('start');
document.getElementById('startbtn').disabled=false;
}
function loop(now){
requestAnimationFrame(loop);
const dt=clamp((now-(last||now))/1000,0,0.05);last=now;
frameNo++;
const pcx=Math.floor(player.x)>>4, pcz=Math.floor(player.z)>>4;
if(state==='loading'){
for(let i=0;i<6&&loadIdx<loadList.length;i++,loadIdx++){
const o=loadList[loadIdx];
buildChunkMeshes(pcx+o[0],pcz+o[1]);
}
const pct=loadList.length?Math.round(100*loadIdx/loadList.length):100;
document.getElementById('loadfill').style.width=pct+'%';
document.getElementById('loadtext').textContent=S('loading')+pct+'%';
if(loadIdx>=loadList.length)finishLoading();
}else if(state==='playing'){
updatePlayer(dt);
chunkTick(pcx,pcz,2);
updateHUD(dt);
}
updateHL();
camera.position.set(player.x,player.y+1.62,player.z);
camera.rotation.set(player.pitch,player.yaw,0);
const tf=player.sprint?82:75;
if(Math.abs(camera.fov-tf)>0.05){camera.fov+=(tf-camera.fov)*Math.min(1,dt*8);camera.updateProjectionMatrix();}
updateSky(state==='playing'?dt:0);
renderer.render(scene,camera);
}
function init(){
document.getElementById('langzh').addEventListener('click',()=>setLang('zh'));
document.getElementById('langen').addEventListener('click',()=>setLang('en'));
buildAtlas();
setupThree();
buildHotbar();
applyLang();
respawn();
if('ontouchstart' in window)document.getElementById('mobilenote').style.display='block';
loadList=OFFS.filter(o=>o[2]<=RD*RD);
state='loading';
requestAnimationFrame(loop);
}
init();
</script>
</body>
</html>