Files
MRCC/prototype/voxel-world.html

3315 lines
138 KiB
HTML
Raw Permalink 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">
<title>RedCircuit · 方块世界 v0.4</title>
<style>
/* ============================================================
RedCircuit · 方块世界 — 启动器视觉层 v3(红蓝白 · 方形科技风)
品牌红 #E83229 | 辅助蓝 #58A6FF | 白 #F5F7FB
============================================================ */
:root{
color-scheme:light;
--red:#E83229;
--red-deep:#C4201E;
--red-bright:#E83229;
--blue:#2D7FF0;
--blue-deep:#1D5FBF;
--white:#FFFFFF;
--bg-0:#f2f4f8;
--panel-grad:linear-gradient(180deg,rgba(255,255,255,0.94),rgba(242,244,248,0.97));
--line:rgba(10,15,30,0.10);
--line-hi:rgba(10,15,30,0.20);
--t1:#1b1d26;
--t2:#4a5160;
--t3:#8b93a3;
--r-lg:4px;
--r-md:3px;
--r-sm:2px;
--font:'Segoe UI','Microsoft YaHei',sans-serif;
--shadow-deep:0 20px 50px rgba(15,20,40,0.18),0 0 30px rgba(232,50,41,0.04);
}
/* ---------- 基础 ---------- */
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
overflow:hidden;
background:var(--bg-0);
color:var(--t1);
font-family:var(--font);
font-size:14px;
line-height:1.55;
text-rendering:optimizeLegibility;
-webkit-font-smoothing:antialiased;
}
canvas{display:block}
/* 背景画布:压暗增饱和,作为优雅底色 */
#bgCanvas{
position:fixed;inset:0;z-index:0;width:100%;height:100%;object-fit:cover;
filter:brightness(0.7) saturate(1.06);
}
/* ---------- 游戏内 HUD ---------- */
#crosshair{position:fixed;top:50%;left:50%;width:20px;height:20px;transform:translate(-50%,-50%);pointer-events:none;z-index:10}
#crosshair::before,#crosshair::after{content:'';position:absolute;background:rgba(255,255,255,0.75);animation:crosshair-pulse 2s ease-in-out infinite}
#crosshair::before{width:2px;height:12px;left:9px;top:4px;box-shadow:0 0 4px rgba(232,50,41,0.35)}
#crosshair::after{width:12px;height:2px;left:4px;top:9px;box-shadow:0 0 4px rgba(232,50,41,0.35)}
#hotbar{
position:fixed;bottom:12px;left:50%;transform:translateX(-50%);
display:flex;gap:3px;padding:5px;
background:linear-gradient(180deg,rgba(255,255,255,0.88),rgba(244,246,250,0.94));
backdrop-filter:blur(8px);
-webkit-backdrop-filter:blur(8px);
border:1px solid var(--line);
border-radius:2px;z-index:10;
box-shadow:0 8px 26px rgba(15,20,40,0.18),inset 0 1px 0 rgba(255,255,255,0.9);
}
.slot{
width:52px;height:58px;
border:2px solid;
border-color:#dfe3ec #c6ccda #c6ccda #dfe3ec;
border-radius:2px;
display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
cursor:pointer;position:relative;
background:linear-gradient(180deg,#ffffff,#f1f3f8);
padding-top:4px;
transition:border-color .15s,box-shadow .15s,transform .15s;
}
.slot:hover{border-color:#b6bdcd #9aa3b8 #9aa3b8 #b6bdcd;transform:translateY(-1px)}
.slot.active{
border-color:#ff6a5e !important;
box-shadow:0 0 0 1px rgba(232,50,41,0.6),0 0 12px rgba(232,50,41,0.35) inset,0 0 8px rgba(232,50,41,0.3);
}
.slot-icon{width:30px;height:30px;image-rendering:pixelated}
.slot-key{position:absolute;top:2px;left:3px;font-size:8px;color:rgba(27,29,38,0.45)}
.slot-name{font-size:8px;color:rgba(27,29,38,0.68);margin-top:3px;text-align:center;line-height:1.1;white-space:nowrap;max-width:46px;overflow:hidden;text-overflow:ellipsis}
#info{
position:fixed;top:14px;left:14px;
color:var(--t2);font-size:12px;z-index:10;line-height:1.7;
background:linear-gradient(180deg,rgba(255,255,255,0.88),rgba(244,246,250,0.94));
backdrop-filter:blur(6px);
-webkit-backdrop-filter:blur(6px);
padding:9px 14px;border-radius:2px;
border:1px solid var(--line);
box-shadow:0 6px 20px rgba(15,20,40,0.14);
}
#info b{color:var(--red);text-shadow:0 0 10px rgba(232,50,41,0.35);animation:breathe 3.2s ease-in-out infinite}
/* ---------- 全屏遮罩(浅色红调渐变,白色主题) ---------- */
#overlay{
position:fixed;inset:0;
background:
radial-gradient(120% 85% at 50% 0%,rgba(232,50,41,0.08),transparent 60%),
linear-gradient(168deg,rgba(246,248,252,0.82) 0%,rgba(238,241,247,0.92) 100%);
backdrop-filter:blur(2px);
-webkit-backdrop-filter:blur(2px);
display:flex;align-items:center;justify-content:center;
z-index:100;color:var(--t1);
}
/* ---------- 菜单面板(玻璃 · 左对齐按钮) ---------- */
.menu-panel{
display:flex;flex-direction:column;align-items:stretch;
text-align:left;
width:min(92vw,400px);min-width:300px;max-width:400px;
max-height:88vh;overflow-y:auto;
padding:36px 38px;
background:var(--panel-grad);
backdrop-filter:blur(20px) saturate(1.25);
-webkit-backdrop-filter:blur(20px) saturate(1.25);
border:1px solid var(--line);
border-top-color:rgba(255,255,255,0.9);
border-radius:var(--r-lg);
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
}
.menu-panel h1{
font-size:30px;font-weight:700;letter-spacing:2px;
color:var(--red);text-align:center;
margin-bottom:6px;
text-shadow:0 2px 14px rgba(232,50,41,0.18);
}
.menu-panel h2{
font-size:19px;font-weight:600;color:var(--t1);
text-align:center;margin-bottom:16px;letter-spacing:1px;
}
.menu-panel p{
color:var(--t2);margin:2px 0 22px;font-size:13px;text-align:center;letter-spacing:2px;
}
.menu-panel .keys{margin-top:18px;font-size:12px;color:var(--t3);line-height:2;text-align:center}
.menu-panel .keys b{color:var(--red);font-weight:600}
/* 面板内:按钮列 / 表单 / 状态 统一左对齐(280px 内容列) */
.menu-panel > .menu-btn,
.menu-panel > .form-group,
.menu-panel > .form-error,
.menu-panel > .menu-status{
align-self:flex-start;width:100%;max-width:280px;
}
.menu-panel > .menu-btn{justify-content:flex-start;text-align:left}
/* ---------- 按钮 ---------- */
.menu-btn{
display:inline-flex;align-items:center;justify-content:center;gap:6px;
padding:11px 22px;margin:6px 0;
background:rgba(30,40,70,0.07);
border:1px solid var(--line);
color:var(--t1);font-size:14px;font-family:inherit;
border-radius:var(--r-md);cursor:pointer;
transition:background .18s,border-color .18s,color .18s,transform .18s,box-shadow .18s;
user-select:none;
}
.menu-btn:hover{
background:rgba(30,40,70,0.13);
border-color:var(--line-hi);
color:var(--t1);
transform:translateY(-1px);
box-shadow:0 6px 16px rgba(20,30,60,0.14);
}
.menu-btn:active{transform:translateY(0) scale(0.98)}
.menu-btn.primary{
position:relative;overflow:hidden;
background:linear-gradient(180deg,#f0473a,#d8231b);
border-color:rgba(232,50,41,0.6);
color:#fff;font-weight:600;letter-spacing:0.5px;
box-shadow:0 8px 22px rgba(232,50,41,0.28),inset 0 1px 0 rgba(255,255,255,0.20);
}
.menu-btn.primary:hover{
background:linear-gradient(180deg,#ff5749,#e32b21);
box-shadow:0 10px 28px rgba(232,50,41,0.36),inset 0 1px 0 rgba(255,255,255,0.22);
}
.menu-btn.primary::after{
content:'';position:absolute;inset:0;pointer-events:none;
background:linear-gradient(90deg,transparent,rgba(255,255,255,0.07),transparent);
background-size:200% 100%;
animation:shimmer 3.5s ease-in-out infinite;
}
.menu-btn.ghost{background:transparent;border-color:transparent;color:var(--t2)}
.menu-btn.ghost:hover{background:rgba(30,40,70,0.06);color:var(--t1);box-shadow:none}
.menu-btn.start-btn{margin-top:18px;padding:14px 40px;font-size:16px;font-weight:600}
/* ---------- 表单 ---------- */
.form-group{width:100%;max-width:280px;margin:6px 0}
.form-group input{
width:100%;padding:12px 14px;
background:rgba(255,255,255,0.85);
border:1px solid var(--line);
color:var(--t1);font-size:13px;font-family:inherit;
border-radius:var(--r-sm);
outline:none;
transition:border-color .18s,box-shadow .18s,background .18s;
}
.form-group input::placeholder{color:#98a0b0}
.form-group input:focus{
border-color:rgba(232,50,41,0.65);
background:#fff;
box-shadow:0 0 0 3px rgba(232,50,41,0.12),0 0 16px rgba(232,50,41,0.10);
}
.form-error{width:100%;max-width:280px;min-height:20px;font-size:12px;color:var(--red);margin:3px 0;text-align:left}
.menu-status{font-size:11px;color:var(--t3);margin-top:18px;text-align:left}
/* ---------- Hub 主页 ---------- */
.hub-page{position:absolute;inset:0;display:flex}
.hub-profile{
position:absolute;top:24px;left:24px;
display:flex;align-items:center;flex-wrap:wrap;gap:12px;
background:linear-gradient(180deg,rgba(255,255,255,0.94),rgba(244,246,250,0.97));
backdrop-filter:blur(16px);
-webkit-backdrop-filter:blur(16px);
padding:12px 18px;border-radius:var(--r-md);
border:1px solid var(--line);
box-shadow:0 12px 34px rgba(15,20,40,0.14),inset 0 1px 0 rgba(255,255,255,0.9);
z-index:2;
max-width:min(92vw,740px);
}
.hub-profile canvas{border-radius:2px;image-rendering:pixelated}
#hubAvatar{border:2px solid rgba(232,50,41,0.35);box-shadow:0 0 14px rgba(232,50,41,0.20)}
.hub-profile-info{display:flex;flex-direction:column}
.hub-nickname{color:var(--t1);font-size:15px;font-weight:600}
.hub-level{color:var(--red);font-size:11px;font-weight:600;animation:breathe 2.4s ease-in-out infinite}
.hub-coins{display:flex;align-items:center;gap:2px;font-size:12px;color:var(--t2);margin-top:2px}
.coin-icon{font-size:14px}
/* 用户信息卡内的横向小按钮 */
.hub-profile .menu-btn{
width:auto;margin:0;padding:6px 13px;
font-size:11px;border-radius:2px;
background:rgba(30,40,70,0.06);border-color:var(--line);
}
.hub-profile .menu-btn:hover{background:rgba(30,40,70,0.12);border-color:var(--line-hi)}
.hub-start-btn{
position:absolute;bottom:36px;right:36px;
padding:17px 38px;
background:linear-gradient(180deg,#f0473a,#d8231b);
border:1px solid rgba(232,50,41,0.6);
color:#fff;font-size:19px;font-weight:600;letter-spacing:1px;
border-radius:2px;cursor:pointer;
display:flex;align-items:center;gap:10px;
box-shadow:0 12px 36px rgba(232,50,41,0.34),inset 0 1px 0 rgba(255,255,255,0.22);
transition:transform .2s,box-shadow .2s,background .2s;
overflow:hidden;
}
.hub-start-btn::before{
content:'';position:absolute;inset:0;pointer-events:none;
background:linear-gradient(90deg,transparent,rgba(255,255,255,0.08),transparent);
background-size:200% 100%;
animation:shimmer 4s ease-in-out infinite;
}
.hub-start-btn:hover{
transform:translateY(-3px);
background:linear-gradient(180deg,#ff5749,#e32b21);
box-shadow:0 16px 44px rgba(232,50,41,0.46),inset 0 1px 0 rgba(255,255,255,0.24);
}
.btn-arrow{font-size:16px;display:inline-block;animation:float 2.4s ease-in-out infinite}
/* ---------- 游戏模式选择 ---------- */
.modes-page{
display:flex;flex-direction:column;align-items:center;
min-width:0;width:min(92vw,660px);max-width:92vw;
padding:32px 34px;
background:var(--panel-grad);
backdrop-filter:blur(20px) saturate(1.25);
-webkit-backdrop-filter:blur(20px) saturate(1.25);
border:1px solid var(--line);
border-radius:2px;
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
max-height:88vh;overflow-y:auto;
}
.modes-title{
font-size:20px;font-weight:600;color:var(--t1);
margin-bottom:24px;letter-spacing:2px;text-align:center;
}
.modes-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;width:100%;margin-bottom:20px;min-width:0}
.mode-card{
background:linear-gradient(180deg,rgba(255,255,255,0.9),rgba(244,246,250,0.92));
border:1px solid var(--line);
border-radius:2px;padding:18px 14px;text-align:center;cursor:pointer;
min-width:0;
transition:transform .2s,border-color .2s,box-shadow .2s,background .2s;
}
.mode-card:hover{
transform:translateY(-4px);
border-color:rgba(232,50,41,0.45);
background:linear-gradient(180deg,#fff,#fdf2f1);
box-shadow:0 12px 26px rgba(15,20,40,0.12),0 0 18px rgba(232,50,41,0.08);
}
.mode-card.active{
border-color:rgba(232,50,41,0.85);
background:linear-gradient(180deg,#fff5f4,#fdebe9);
box-shadow:0 0 0 1px rgba(232,50,41,0.55),0 0 22px rgba(232,50,41,0.14);
}
.mode-icon{font-size:28px;margin-bottom:10px;transition:transform .25s}
.mode-card:hover .mode-icon{transform:scale(1.12)}
.mode-name{color:var(--t1);font-size:14px;font-weight:600;margin-bottom:6px}
.mode-card.active .mode-name{color:var(--red);}
.mode-desc{color:var(--t3);font-size:10px;line-height:1.6}
.modes-page .menu-btn{width:100%;max-width:340px}
/* ---------- 方块破坏进度 / 粒子 ---------- */
#breakProgress{position:fixed;pointer-events:none;z-index:11;display:none}
#breakProgress .crack-stage{position:absolute;inset:0;background-size:cover;image-rendering:pixelated}
.particle{position:fixed;pointer-events:none;z-index:8;border-radius:1px}
/* ---------- 商店 ---------- */
.shop-page{
display:flex;flex-direction:column;align-items:center;
min-width:0;width:min(92vw,560px);max-width:92vw;
padding:28px 28px;
background:var(--panel-grad);
backdrop-filter:blur(20px) saturate(1.25);
-webkit-backdrop-filter:blur(20px) saturate(1.25);
border:1px solid var(--line);
border-radius:2px;
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
max-height:82vh;overflow-y:auto;
}
.shop-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;width:100%;margin-bottom:14px;min-width:0}
.shop-item{
background:linear-gradient(180deg,rgba(255,255,255,0.9),rgba(244,246,250,0.92));
border:1px solid var(--line);
border-radius:2px;padding:15px 12px;text-align:center;
transition:transform .2s,border-color .2s,box-shadow .2s;
}
.shop-item:hover{transform:translateY(-3px);border-color:rgba(232,50,41,0.4);box-shadow:0 10px 20px rgba(15,20,40,0.10),0 0 14px rgba(232,50,41,0.06)}
.shop-item-icon{font-size:28px;margin-bottom:8px;transition:transform .25s}
.shop-item:hover .shop-item-icon{transform:scale(1.1)}
.shop-item-name{color:var(--t1);font-size:13px;font-weight:600;margin-bottom:5px}
.shop-item-desc{color:var(--t3);font-size:10px;margin-bottom:10px;line-height:1.5}
.shop-item-price{display:flex;align-items:center;justify-content:center;gap:4px;font-size:12px;font-weight:600;margin-bottom:10px}
.shop-item-price.diamonds{color:var(--blue-deep)}
.shop-item-price.redstone{color:var(--red)}
.shop-item-price.gold{color:var(--blue)}
.shop-buy-btn{
padding:5px 16px;
border:1px solid rgba(232,50,41,0.45);
border-radius:2px;
background:rgba(232,50,41,0.10);
color:var(--red);font-size:11px;font-weight:600;font-family:inherit;cursor:pointer;
transition:background .18s,border-color .18s,transform .15s;
}
.shop-buy-btn:hover{background:rgba(232,50,41,0.18);border-color:rgba(232,50,41,0.65)}
.shop-buy-btn:active{transform:scale(0.94)}
.shop-balance{margin-top:6px;font-size:12px;color:var(--t2);text-align:center}
/* ---------- 背包(市场面板) ---------- */
.inv-panel-page{
display:flex;flex-direction:column;align-items:center;
min-width:0;width:min(92vw,520px);max-width:92vw;
padding:28px 30px;
background:var(--panel-grad);
backdrop-filter:blur(20px) saturate(1.25);
-webkit-backdrop-filter:blur(20px) saturate(1.25);
border:1px solid var(--line);
border-radius:2px;
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
max-height:82vh;overflow-y:auto;
}
.inv-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;width:100%;margin-bottom:14px;min-width:0}
.inv-item{
background:linear-gradient(180deg,rgba(255,255,255,0.9),rgba(244,246,250,0.92));
border:1px solid var(--line);
border-radius:2px;padding:13px 9px;text-align:center;
transition:transform .2s,border-color .2s;
}
.inv-item:hover{transform:translateY(-2px);border-color:rgba(45,127,240,0.4)}
.inv-item-icon{font-size:24px;margin-bottom:5px}
.inv-item-name{color:var(--t1);font-size:11px;margin-bottom:3px}
.inv-item-count{color:var(--blue);font-size:10px;font-weight:600;margin-bottom:7px}
.inv-use-btn{
padding:4px 12px;
background:rgba(45,127,240,0.10);
border:1px solid rgba(45,127,240,0.30);
color:var(--blue);font-size:10px;font-weight:600;font-family:inherit;
border-radius:2px;cursor:pointer;
transition:background .18s,border-color .18s;
}
.inv-use-btn:hover{background:rgba(45,127,240,0.18);border-color:rgba(45,127,240,0.50)}
.inv-history{margin-top:12px;font-size:10px;color:var(--t3);width:100%;max-height:130px;overflow-y:auto}
.inv-history-title{font-size:11px;color:var(--t2);margin-bottom:6px;letter-spacing:1px}
.inv-history-item{padding:4px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between}
/* ---------- Toast ---------- */
#toast{
position:fixed;top:50%;left:50%;transform:translate(-50%,-60px);
background:rgba(255,255,255,0.92);
backdrop-filter:blur(10px);
-webkit-backdrop-filter:blur(10px);
color:var(--t1);padding:9px 18px;border-radius:2px;font-size:13px;z-index:20;
opacity:0;transition:opacity .3s;pointer-events:none;
border:1px solid var(--line-hi);
box-shadow:0 8px 28px rgba(15,20,40,0.18);
}
#toast.show{opacity:1}
/* ---------- 游戏内背包(MC 像素风) ---------- */
#inventory{
position:fixed;inset:0;background:rgba(242,244,248,0.55);
backdrop-filter:blur(4px);
-webkit-backdrop-filter:blur(4px);
display:none;align-items:center;justify-content:center;z-index:50;
}
#inventory.show{display:flex}
.inv-panel{
background:var(--panel-grad);
border:1px solid var(--line);
border-radius:2px;padding:14px;
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
display:flex;gap:12px;
max-width:94vw;max-height:90vh;overflow:auto;
}
.inv-left{display:flex;flex-direction:column;align-items:center}
.inv-right{display:flex;flex-direction:column;align-items:center}
.inv-title{font-size:11px;color:var(--t2);font-weight:600;margin-bottom:8px;text-align:center;letter-spacing:2px;text-transform:uppercase}
.inv-storage{display:grid;grid-template-columns:repeat(9,36px);gap:2px;justify-content:center;margin-bottom:8px}
.inv-hotbar{display:grid;grid-template-columns:repeat(10,36px);gap:2px;justify-content:center;border-top:1px solid var(--line);padding-top:8px;margin-top:4px}
.inv-creative{
display:grid;grid-template-columns:repeat(8,36px);gap:2px;justify-content:center;
width:300px;max-height:420px;overflow-y:auto;padding:6px;
background:rgba(244,246,250,0.6);border:1px solid var(--line);border-radius:2px;
}
.inv-creative::-webkit-scrollbar{width:6px}
.inv-creative::-webkit-scrollbar-track{background:rgba(10,15,30,0.05)}
.inv-creative::-webkit-scrollbar-thumb{background:rgba(10,15,30,0.25);border-radius:2px}
.inv-slot{
width:36px;height:36px;
background:linear-gradient(180deg,#ffffff,#eef0f5);
border:2px solid;border-color:#dfe3ec #c6ccda #c6ccda #dfe3ec;
border-radius:2px;
display:flex;align-items:center;justify-content:center;cursor:pointer;position:relative;
transition:background .12s,border-color .12s,box-shadow .12s;
}
.inv-slot:hover{background:linear-gradient(180deg,#f7f8fb,#e4e8f0);border-color:#b6bdcd #9aa3b8 #9aa3b8 #b6bdcd}
.inv-slot.active{border-color:#ff6a5e;box-shadow:0 0 0 1px rgba(232,50,41,0.55),0 0 8px rgba(232,50,41,0.4) inset}
.inv-slot canvas,.inv-slot img{width:26px;height:26px;image-rendering:pixelated;pointer-events:none}
.inv-slot .slot-num{position:absolute;top:1px;left:2px;font-size:7px;color:rgba(27,29,38,0.45);pointer-events:none}
.inv-cat-label{font-size:8px;color:var(--t3);margin:4px 0 2px;padding-left:2px;text-align:left;width:100%;letter-spacing:0.5px}
#carriedItem{position:fixed;width:36px;height:36px;pointer-events:none;z-index:60;display:none}
#carriedItem canvas,#carriedItem img{width:26px;height:26px;image-rendering:pixelated}
.inv-hint{text-align:center;font-size:8px;color:var(--t3);margin-top:6px}
/* ---------- Loading ---------- */
#loading{
position:fixed;inset:0;
background:rgba(246,248,252,0.82);
backdrop-filter:blur(6px);
-webkit-backdrop-filter:blur(6px);
display:flex;align-items:center;justify-content:center;flex-direction:column;
z-index:200;color:var(--t1);
}
#loading .bar{width:220px;height:5px;background:rgba(10,15,30,0.12);border-radius:2px;margin-top:14px;overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,0.15)}
#loading .fill{height:100%;background:linear-gradient(90deg,#d8231b,#ff5a45);width:0;transition:width .4s ease-out;box-shadow:0 0 14px rgba(232,50,41,0.5)}
/* ---------- AI 助手 ---------- */
#aiBtn{
position:fixed;bottom:92px;right:22px;width:46px;height:46px;
background:linear-gradient(180deg,#f0473a,#d8231b);
border:2px solid rgba(255,255,255,0.22);
border-radius:50%;color:#fff;font-size:20px;cursor:pointer;z-index:15;
display:none;align-items:center;justify-content:center;
box-shadow:0 6px 22px rgba(232,50,41,0.45);
animation:glow-pulse 4s ease-in-out infinite;
transition:transform .2s,box-shadow .2s;
}
#aiBtn:hover{transform:scale(1.12) rotate(10deg);box-shadow:0 8px 30px rgba(232,50,41,0.6)}
#aiBtn.show{display:flex}
#aiChat{
position:fixed;inset:0;background:rgba(242,244,248,0.55);
backdrop-filter:blur(3px);
-webkit-backdrop-filter:blur(3px);
display:none;align-items:center;justify-content:center;z-index:70;
}
#aiChat.show{display:flex}
#aiChat.pointer-locked{display:none}
.ai-panel{
width:min(92vw,420px);max-width:420px;max-height:min(88vh,520px);
background:var(--panel-grad);
border:1px solid var(--line);
border-radius:2px;display:flex;flex-direction:column;
box-shadow:var(--shadow-deep),0 0 30px rgba(232,50,41,0.04),inset 0 1px 0 rgba(255,255,255,0.9);
}
.ai-header{
display:flex;align-items:center;justify-content:space-between;
padding:12px 16px;border-bottom:1px solid var(--line);
background:linear-gradient(180deg,rgba(232,50,41,0.06),transparent);
border-radius:2px 2px 0 0;
}
.ai-header span{color:var(--t1);font-size:14px;font-weight:600;letter-spacing:0.5px}
.ai-close{background:none;border:none;color:var(--t3);font-size:18px;cursor:pointer;padding:2px 6px;border-radius:2px;transition:color .15s,background .15s}
.ai-close:hover{color:var(--t1);background:rgba(30,40,70,0.08)}
.ai-messages{flex:1;overflow-y:auto;padding:12px 16px;min-height:200px;max-height:320px;display:flex;flex-direction:column;gap:8px}
.ai-messages::-webkit-scrollbar{width:5px}
.ai-messages::-webkit-scrollbar-track{background:rgba(30,40,70,0.06)}
.ai-messages::-webkit-scrollbar-thumb{background:rgba(110,120,140,0.5);border-radius:2px}
.ai-msg{font-size:12px;line-height:1.6;padding:9px 12px;border-radius:2px;max-width:88%;word-break:break-word;animation:slide-up .25s ease-out}
.ai-msg.user{align-self:flex-end;background:rgba(232,50,41,0.10);color:var(--t1);border:1px solid rgba(232,50,41,0.25)}
.ai-msg.ai{align-self:flex-start;background:rgba(244,246,250,0.9);color:var(--t2);border:1px solid var(--line)}
.ai-msg .label{font-size:9px;color:var(--t3);margin-bottom:4px;font-weight:600;letter-spacing:0.5px}
.ai-input-row{display:flex;padding:10px 16px;border-top:1px solid var(--line);gap:8px}
.ai-input-row input{
flex:1;padding:9px 12px;
background:rgba(255,255,255,0.9);
border:1px solid var(--line);
color:var(--t1);font-size:12px;font-family:inherit;border-radius:2px;outline:none;
transition:border-color .18s,box-shadow .18s;
}
.ai-input-row input:focus{border-color:rgba(232,50,41,0.65);box-shadow:0 0 0 3px rgba(232,50,41,0.12)}
.ai-input-row button{
padding:9px 16px;background:linear-gradient(180deg,#f0473a,#d8231b);
border:1px solid rgba(232,50,41,0.5);color:#fff;font-size:12px;font-weight:600;
border-radius:2px;cursor:pointer;font-family:inherit;
box-shadow:0 4px 14px rgba(232,50,41,0.28);
transition:background .18s,box-shadow .18s;
}
.ai-input-row button:hover{background:linear-gradient(180deg,#ff5749,#e32b21);box-shadow:0 6px 18px rgba(232,50,41,0.38)}
.ai-hint{text-align:center;font-size:10px;color:var(--t3);padding:6px 0 8px}
/* ====== 关键动画(全部 keyframes 保留) ====== */
@keyframes glow-pulse{0%,100%{box-shadow:0 0 6px rgba(232,50,41,0.15),0 0 16px rgba(232,50,41,0.05)}50%{box-shadow:0 0 12px rgba(232,50,41,0.35),0 0 26px rgba(232,50,41,0.14)}}
@keyframes glow-soft{0%,100%{box-shadow:0 0 3px rgba(232,50,41,0.10)}50%{box-shadow:0 0 9px rgba(232,50,41,0.22)}}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
@keyframes breathe{0%,100%{opacity:0.55}50%{opacity:1}}
@keyframes shimmer{0%{background-position:-200% center}100%{background-position:200% center}}
@keyframes border-glow{0%,100%{border-color:rgba(232,50,41,0.30)}50%{border-color:rgba(232,50,41,0.70)}}
@keyframes slide-up{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
@keyframes particle-drift{0%{transform:translate(0,0);opacity:0}10%{opacity:0.8}90%{opacity:0.3}100%{transform:translate(var(--dx),var(--dy));opacity:0}}
@keyframes crosshair-pulse{0%,100%{opacity:0.5}50%{opacity:0.9}}
/* ---------- 背景粒子 ---------- */
.menu-particles{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.menu-particle{position:absolute;width:2px;height:2px;background:rgba(232,50,41,0.5);border-radius:50%;animation:particle-drift 6s ease-in-out infinite}
.menu-particle:nth-child(odd){background:rgba(255,255,255,0.30);animation-duration:8s}
#menuParticles{position:fixed;inset:0;pointer-events:none;z-index:99}
/* ---------- 每日签到 ---------- */
.daily-btn{transition:all .3s}
.daily-btn:hover{text-shadow:0 0 8px rgba(88,166,255,0.4)!important}
/* ---------- 游戏内模式 HUD ---------- */
#modeHUD{position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:12;display:none;flex-direction:column;align-items:center;gap:6px;pointer-events:none}
.mode-hud-badge{
background:linear-gradient(180deg,rgba(240,71,58,0.92),rgba(216,35,27,0.92));
color:#fff;padding:6px 20px;border-radius:2px;font-size:13px;font-weight:600;letter-spacing:1px;
text-shadow:0 1px 3px rgba(0,0,0,0.4);
backdrop-filter:blur(6px);
-webkit-backdrop-filter:blur(6px);
border:1px solid rgba(255,255,255,0.14);
box-shadow:0 6px 18px rgba(0,0,0,0.35);
animation:glow-soft 3s ease-in-out infinite;
}
.mode-hud-obj{background:rgba(255,255,255,0.88);color:var(--t2);padding:7px 18px;border-radius:2px;font-size:11px;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border:1px solid var(--line);display:none}
.mode-hud-timer{font-size:36px;font-weight:700;color:#1b1d26;text-shadow:0 0 22px rgba(232,50,41,0.4);display:none;letter-spacing:3px}
.mode-hud-stars{display:none;font-size:20px;gap:4px}
.mode-hud-star{opacity:0.25;transition:opacity .3s}
.mode-hud-star.earned{opacity:1;text-shadow:0 0 8px rgba(245,197,66,0.6)}
.mode-hud-score{display:none;color:var(--blue);font-size:14px;font-weight:600}
/* ---------- 蓝图市场 ---------- */
.bp-page{
display:flex;flex-direction:column;align-items:center;
min-width:0;width:min(92vw,640px);max-width:92vw;
padding:28px 30px;
background:var(--panel-grad);
backdrop-filter:blur(20px) saturate(1.25);
-webkit-backdrop-filter:blur(20px) saturate(1.25);
border:1px solid var(--line);
border-radius:2px;
box-shadow:var(--shadow-deep),inset 0 1px 0 rgba(255,255,255,0.9);
max-height:86vh;overflow-y:auto;
}
.bp-search-row{display:flex;gap:8px;width:100%;margin-bottom:16px}
.bp-search-row input{
flex:1;padding:9px 13px;
background:rgba(255,255,255,0.9);
border:1px solid var(--line);
color:var(--t1);font-size:12px;font-family:inherit;border-radius:2px;outline:none;
transition:border-color .18s,box-shadow .18s;
}
.bp-search-row input:focus{border-color:rgba(232,50,41,0.65);box-shadow:0 0 0 3px rgba(232,50,41,0.12)}
.bp-search-row select{
padding:9px 12px;
background:#fff;
border:1px solid var(--line);
color:var(--t2);font-size:12px;font-family:inherit;border-radius:2px;outline:none;cursor:pointer;
}
.bp-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;width:100%;margin-bottom:14px;overflow-y:auto;max-height:280px;min-width:0}
.bp-card{
background:linear-gradient(180deg,rgba(255,255,255,0.9),rgba(244,246,250,0.92));
border:1px solid var(--line);
border-radius:2px;padding:13px;
transition:transform .2s,border-color .2s,box-shadow .2s;
}
.bp-card:hover{transform:translateY(-2px);border-color:rgba(232,50,41,0.4);box-shadow:0 8px 18px rgba(15,20,40,0.10)}
.bp-card-name{color:var(--t1);font-size:13px;font-weight:600;margin-bottom:4px}
.bp-card-desc{color:var(--t3);font-size:10px;margin-bottom:8px;line-height:1.5}
.bp-card-meta{display:flex;justify-content:space-between;align-items:center}
.bp-card-stars{color:var(--blue);font-size:11px;font-weight:600}
.bp-card-price{color:var(--blue);font-size:11px;font-weight:600}
.bp-card-btn{
padding:4px 12px;
background:rgba(45,127,240,0.10);
border:1px solid rgba(45,127,240,0.30);
color:var(--blue);font-size:10px;font-weight:600;font-family:inherit;
border-radius:2px;cursor:pointer;
transition:background .18s,border-color .18s;
}
.bp-card-btn:hover{background:rgba(45,127,240,0.18);border-color:rgba(45,127,240,0.50)}
.bp-tabs{display:flex;gap:6px;margin-bottom:12px}
.bp-tab{
padding:6px 16px;
background:rgba(10,15,30,0.06);
border:1px solid var(--line);
color:var(--t3);font-size:11px;font-family:inherit;
border-radius:2px;cursor:pointer;
transition:background .18s,color .18s,border-color .18s;
}
.bp-tab:hover{color:var(--t1);border-color:var(--line-hi);background:rgba(10,15,30,0.10)}
.bp-tab.active{background:rgba(232,50,41,0.10);border-color:rgba(232,50,41,0.50);color:var(--red-bright);font-weight:600}
.bp-upload-form{margin-top:10px;display:flex;flex-direction:column;gap:8px;width:100%}
.bp-upload-form input,.bp-upload-form select{
padding:8px 12px;
background:rgba(255,255,255,0.9);
border:1px solid var(--line);
color:var(--t1);font-size:11px;font-family:inherit;border-radius:2px;outline:none;
}
.bp-upload-form input:focus,.bp-upload-form select:focus{border-color:rgba(232,50,41,0.70);box-shadow:0 0 0 3px rgba(232,50,41,0.14)}
/* ---------- 统一精致滚动条 ---------- */
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(10,15,30,0.18);border-radius:2px;border:2px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:rgba(232,50,41,0.35);border:2px solid transparent;background-clip:content-box}
/* ---------- 响应式 ---------- */
@media (max-width:820px){
.modes-grid{grid-template-columns:repeat(2,1fr)}
.shop-grid{grid-template-columns:repeat(2,1fr)}
.inv-grid{grid-template-columns:repeat(3,1fr)}
.hub-profile{left:14px;top:14px;max-width:calc(100vw - 28px)}
.hub-start-btn{right:18px;bottom:18px;padding:14px 26px;font-size:16px}
.inv-panel{flex-direction:column;align-items:center}
.modes-page,.shop-page,.inv-panel-page,.bp-page{width:94vw}
}
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<div id="loading"><div id="loadMsg" style="font-size:16px;font-weight:bold;margin-bottom:4px">正在初始化...</div><div class="bar"><div class="fill" id="loadFill"></div></div><div id="loadSub" style="font-size:10px;color:#8b93a3;margin-top:4px"></div></div>
<div id="debugHUD" style="position:fixed;top:8px;right:8px;z-index:250;font-size:10px;color:#0f0;font-family:Consolas,monospace;background:rgba(0,0,0,0.7);padding:4px 8px;border-radius:2px;pointer-events:none;line-height:1.4;display:none"></div>
<div id="crosshair"></div>
<div id="menuParticles"></div>
<div id="hotbar"></div>
<div id="info">
<div><b>RedCircuit</b> 方块世界 v0.3</div>
<div>位置: <span id="pos">0, 0, 0</span></div>
<div>方块: <span id="bcount">0</span> | <span id="fps">0</span> FPS</div>
<div>看向: <span id="looking">-</span></div>
<div style="color:#e83229;font-size:10px">Enter AI | F11 鼠标</div>
</div>
<div id="toast"></div>
<!-- 游戏内模式 HUD -->
<div id="modeHUD">
<div id="modeHUD-badge" class="mode-hud-badge">自由搭建</div>
<div id="modeHUD-objective" class="mode-hud-obj"></div>
<div id="modeHUD-timer" class="mode-hud-timer"></div>
<div id="modeHUD-stars" class="mode-hud-stars"></div>
<div id="modeHUD-score" class="mode-hud-score"></div>
</div>
<div id="inventory">
<div class="inv-panel">
<div class="inv-left">
<div class="inv-title">方块选取</div>
<div class="inv-creative" id="invCreative"></div>
</div>
<div class="inv-right">
<div class="inv-title">背包</div>
<div class="inv-storage" id="invStorage"></div>
<div class="inv-hotbar" id="invHotbar"></div>
<div class="inv-hint">左键拾取/放置 · E 关闭</div>
</div>
</div>
</div>
<div id="carriedItem"></div>
<div id="invTooltip" style="position:fixed;display:none;background:rgba(255,255,255,0.95);color:#1b1d26;padding:4px 10px;border-radius:2px;font-size:12px;pointer-events:none;z-index:65;white-space:nowrap;border:1px solid rgba(10,15,30,0.20)"></div>
<div id="overlay">
<!-- 欢迎面板 -->
<div id="menu-welcome" class="menu-panel">
<h1>RedCircuit</h1>
<p>红石回路 · 方块世界</p>
<button class="menu-btn primary" onclick="showPanel('login')">登录</button>
<button class="menu-btn" onclick="showPanel('register')">注册账号</button>
<button class="menu-btn ghost" onclick="startGame()">离线模式</button>
<div class="menu-status" id="menuStatus">v0.3 · 连接认证服务中...</div>
</div>
<!-- 登录面板 -->
<div id="menu-login" class="menu-panel" style="display:none">
<h2>登录</h2>
<div class="form-group"><input type="text" id="loginUser" placeholder="用户名" autocomplete="username"></div>
<div class="form-group"><input type="password" id="loginPass" placeholder="密码" autocomplete="current-password"></div>
<div class="form-error" id="loginError"></div>
<button class="menu-btn primary" id="loginBtn" onclick="doLogin()">登录</button>
<button class="menu-btn ghost" onclick="showPanel('welcome')">返回</button>
</div>
<!-- 注册面板 -->
<div id="menu-register" class="menu-panel" style="display:none">
<h2>注册账号</h2>
<div class="form-group"><input type="text" id="regUser" placeholder="用户名 (3-20字符)" autocomplete="username"></div>
<div class="form-group"><input type="password" id="regPass" placeholder="密码 (至少6位)" autocomplete="new-password"></div>
<div class="form-group"><input type="password" id="regPass2" placeholder="确认密码" autocomplete="new-password"></div>
<div class="form-error" id="regError"></div>
<button class="menu-btn primary" id="regBtn" onclick="doRegister()">注册</button>
<button class="menu-btn ghost" onclick="showPanel('welcome')">返回</button>
</div>
<!-- Hub 主页 (登录后) -->
<div id="menu-hub" class="hub-page" style="display:none">
<!-- 左上角:用户信息卡片 -->
<div class="hub-profile">
<canvas id="hubAvatar" width="56" height="56"></canvas>
<div class="hub-profile-info">
<span class="hub-nickname" id="hubNickname">玩家</span>
<span class="hub-level" id="hubLevel">Lv.1</span>
</div>
<div class="hub-coins" id="hubCoins">
<span class="coin-icon">💎</span><span id="hubDiamonds">0</span>
<span class="coin-icon" style="margin-left:8px">🪙</span><span id="hubRSCoins">0</span>
<span class="coin-icon" style="margin-left:8px">🟡</span><span id="hubGoldCoins">0</span>
</div>
<button class="menu-btn ghost daily-btn" id="hubDailyBtn" onclick="claimDailyReward()" style="margin-left:12px;font-size:11px;color:#58A6FF">🎁 签到</button>
<button class="menu-btn ghost" onclick="showPanel('shop')" style="margin-left:4px;font-size:11px;color:#58A6FF;display:flex;align-items:center;gap:3px"><img src="textures/ui/store.jpeg" style="width:14px;height:14px;image-rendering:pixelated">商店</button>
<button class="menu-btn ghost" onclick="showPanel('inventory')" style="margin-left:4px;font-size:11px;color:#58A6FF">🎒 背包</button>
<button class="menu-btn ghost" onclick="showPanel('blueprint')" style="margin-left:4px;font-size:11px;color:#58A6FF">📦 蓝图</button>
<button class="menu-btn ghost" id="hubLogout" onclick="doLogout()" style="margin-left:4px;font-size:11px">退出</button>
</div>
<!-- 右下角:开始游戏 -->
<button class="hub-start-btn" id="hubStartBtn" onclick="showGameModes()">
<span>开始游戏</span>
<span class="btn-arrow"></span>
</button>
</div>
<!-- 游戏模式选项卡 -->
<div id="menu-modes" class="modes-page" style="display:none">
<h2 class="modes-title">选择游戏模式</h2>
<div class="modes-grid">
<div class="mode-card active" onclick="selectMode(this, 'creative')">
<div class="mode-icon">🧱</div>
<div class="mode-name">自由搭建</div>
<div class="mode-desc">无限资源,随心创造<br>256×256 画布,20 存档槽位</div>
</div>
<div class="mode-card" onclick="selectMode(this, 'puzzle')">
<div class="mode-icon">🧩</div>
<div class="mode-name">解密闯关</div>
<div class="mode-desc">6 章 240 关信号谜题<br>三星评级系统</div>
</div>
<div class="mode-card" onclick="selectMode(this, 'multiplayer')">
<div class="mode-icon">🌐</div>
<div class="mode-name">多人联机</div>
<div class="mode-desc">实时协作搭建<br>WebSocket &lt;200ms</div>
</div>
<div class="mode-card" onclick="selectMode(this, 'blueprint')">
<div class="mode-icon">📦</div>
<div class="mode-name">蓝图市场</div>
<div class="mode-desc">上传/下载电路蓝图<br>评分 & 搜索</div>
</div>
</div>
<button class="menu-btn primary" id="modeEnterBtn" onclick="enterSelectedMode()">进入游戏</button>
<button class="menu-btn ghost" onclick="showPanel('hub')">返回</button>
</div>
<!-- 商店面板 -->
<div id="menu-shop" class="shop-page" style="display:none">
<h2 class="modes-title">🛒 商店</h2>
<div class="shop-grid" id="shopGrid"></div>
<div class="shop-balance" id="shopBalance"></div>
<button class="menu-btn ghost" onclick="showPanel('hub')">返回</button>
</div>
<!-- 背包面板 -->
<div id="menu-inventory" class="inv-panel-page" style="display:none">
<h2 class="modes-title">🎒 背包</h2>
<div class="inv-grid" id="invGrid"></div>
<div class="inv-history" id="invHistory"></div>
<button class="menu-btn ghost" onclick="showPanel('hub')">返回</button>
</div>
<!-- 蓝图市场面板 -->
<div id="menu-blueprint" class="bp-page" style="display:none">
<h2 class="modes-title">📦 蓝图市场</h2>
<div class="bp-search-row"><input type="text" id="bpSearch" placeholder="搜索蓝图...">
<select id="bpCategory"><option value="all">全部分类</option><option value="logic">逻辑电路</option><option value="clock">时钟电路</option><option value="memory">存储器</option><option value="cpu">处理器</option></select>
</div>
<div class="bp-grid" id="bpGrid"></div>
<div class="bp-tabs">
<button class="bp-tab active" onclick="switchBPTab(this,'market')">市场</button>
<button class="bp-tab" onclick="switchBPTab(this,'mine')">我的蓝图</button>
<button class="bp-tab" onclick="switchBPTab(this,'upload')">上传</button>
</div>
<div class="bp-upload-form" id="bpUploadForm" style="display:none">
<input type="text" id="bpUploadName" placeholder="蓝图名称"><br>
<input type="text" id="bpUploadDesc" placeholder="简介"><br>
<select id="bpUploadCat"><option value="logic">逻辑电路</option><option value="clock">时钟电路</option><option value="memory">存储器</option><option value="cpu">处理器</option></select><br>
<button class="menu-btn primary" onclick="uploadBlueprint()" style="font-size:12px;padding:8px 20px">发布蓝图 (50💎)</button>
</div>
<button class="menu-btn ghost" onclick="showPanel('hub')">返回</button>
</div>
<!-- 暂停面板 -->
<div id="menu-pause" class="menu-panel" style="display:none">
<h2>已暂停</h2>
<button class="menu-btn primary" id="resumeBtn">点击继续</button>
<button class="menu-btn ghost" id="menuBackBtn">返回主菜单</button>
<div class="keys">WASD 移动 · 空格跳跃 · 鼠标视角<br>左键破坏 · 右键放置 · 数字 1-9 快捷栏<br><b>E 打开背包</b> · ESC 暂停</div>
</div>
</div>
<!-- AI 悬浮按钮 -->
<button id="aiBtn" title="AI 助手 (F11释放鼠标后可点击)" onclick="toggleAIChat()"></button>
<!-- AI 对话框 -->
<div id="aiChat">
<div class="ai-panel">
<div class="ai-header">
<span>RedCircuit AI 助手</span>
<button class="ai-close" onclick="closeAIChat()"></button>
</div>
<div class="ai-messages" id="aiMessages">
<div class="ai-msg ai"><span class="label">AI 助手</span>你好!我是 RedCircuit AI 助手。<br>我可以帮你解答电路问题、推荐搭建方案。<br>试试问我:"如何搭建一个 AND 门?"</div>
</div>
<div class="ai-input-row">
<input type="text" id="aiInput" placeholder="输入问题... (Enter 发送, Esc 关闭)" autocomplete="off">
<button onclick="sendAIMessage()">发送</button>
</div>
<div class="ai-hint">F11 释放鼠标 · 按钮在右下角</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ==================== WebGL + Three.js 诊断 ====================
(function(){
var diag=[];
function report(msg){diag.push(msg);console.log('[DIAG] '+msg);}
report('Three.js: '+(typeof THREE!=='undefined'?'loaded v'+THREE.REVISION:'MISSING'));
report('WebGL: '+(typeof WebGLRenderingContext!=='undefined'?'API available':'MISSING'));
// 测试 WebGL 上下文
var c=document.createElement('canvas');
var gl=null;
var ctxNames=['webgl2','webgl','experimental-webgl'];
for(var i=0;i<ctxNames.length;i++){
try{gl=c.getContext(ctxNames[i]);if(gl){report('WebGL context: '+ctxNames[i]+' OK');break;}}
catch(e){report('WebGL '+ctxNames[i]+': '+e.message);}
}
if(!gl){
report('FATAL: No WebGL context available');
window._webglFailed=true;
}else{
report('Renderer: '+gl.getParameter(gl.RENDERER));
report('Vendor: '+gl.getParameter(gl.VENDOR));
var ext=gl.getExtension('WEBGL_debug_renderer_info');
if(ext)report('GPU: '+gl.getParameter(ext.UNMASKED_RENDERER_WEBGL));
}
// 诊断报告注入到页面
window._diagReport=diag;
})();
</script>
<script>
// ==================== Perlin 噪声 ====================
class PerlinNoise {
constructor(seed) {
const p = Array.from({length:256},(_,i)=>i);
let s = seed;
const rand = () => { s = (s*16807)%2147483647; return (s-1)/2147483646; };
for (let i = 255; i > 0; i--) { const j = Math.floor(rand()*(i+1)); [p[i],p[j]]=[p[j],p[i]]; }
this.perm = [...p, ...p];
}
fade(t) { return t*t*t*(t*(t*6-15)+10); }
lerp(a,b,t) { return a+t*(b-a); }
grad(h,x,y) { const u = (h&1)?-x:x; const v = (h&2)?-y:y; return u+v; }
noise(x,y) {
const X = Math.floor(x)&255, Y = Math.floor(y)&255;
x -= Math.floor(x); y -= Math.floor(y);
const u = this.fade(x), v = this.fade(y);
const aa = this.perm[this.perm[X]+Y], ab = this.perm[this.perm[X]+Y+1];
const ba = this.perm[this.perm[X+1]+Y], bb = this.perm[this.perm[X+1]+Y+1];
return this.lerp(this.lerp(this.grad(aa,x,y),this.grad(ba,x-1,y),u), this.lerp(this.grad(ab,x,y-1),this.grad(bb,x-1,y-1),u), v);
}
octave(x,y,oct,pers) {
let t=0,f=1,a=1,mx=0;
for(let i=0;i<oct;i++){t+=this.noise(x*f,y*f)*a;mx+=a;a*=pers;f*=2;}
return t/mx;
}
}
// ==================== 像素材质生成 ====================
const TS = 16; // 纹理尺寸 16x16
function makeTex(drawFn) {
const c = document.createElement('canvas'); c.width = TS; c.height = TS;
const ctx = c.getContext('2d'); drawFn(ctx);
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter;
return tex;
}
// 噪点辅助
function noisePixels(ctx, baseRGB, vary, density) {
const [r,g,b] = baseRGB;
for (let y=0; y<TS; y++) for (let x=0; x<TS; x++) {
const n = (Math.random()-0.5)*vary;
ctx.fillStyle = `rgb(${clamp(r+n)},${clamp(g+n)},${clamp(b+n)})`;
ctx.fillRect(x,y,1,1);
}
}
function clamp(v) { return Math.max(0,Math.min(255,Math.round(v))); }
// 各方块面纹理
const TEX = {
grassTop: makeTex(ctx => {
noisePixels(ctx, [90,140,55], 25);
// 随机深色草点
for(let i=0;i<20;i++){ctx.fillStyle=`rgb(${60+Math.random()*20},${100+Math.random()*30},${40+Math.random()*15})`;ctx.fillRect(Math.random()*16|0,Math.random()*16|0,1,1)}
}),
grassSide: makeTex(ctx => {
noisePixels(ctx, [134,96,67], 20); // 泥土底
// 顶部草层
for(let x=0;x<TS;x++){
const h = 3 + (Math.random()*3|0);
for(let y=0;y<h;y++){
const n=(Math.random()-0.5)*20;
ctx.fillStyle=`rgb(${clamp(90+n)},${clamp(140+n)},${clamp(55+n)})`;
ctx.fillRect(x,y,1,1);
}
}
}),
dirt: makeTex(ctx => {
noisePixels(ctx, [134,96,67], 25);
for(let i=0;i<15;i++){ctx.fillStyle=`rgb(${100+Math.random()*20},${70+Math.random()*15},${45+Math.random()*10})`;ctx.fillRect(Math.random()*16|0,Math.random()*16|0,1,1)}
}),
stone: makeTex(ctx => {
noisePixels(ctx, [128,128,128], 20);
for(let i=0;i<10;i++){ctx.fillStyle=`rgb(${100+Math.random()*30},${100+Math.random()*30},${100+Math.random()*30})`;ctx.fillRect(Math.random()*16|0,Math.random()*16|0,2,1)}
}),
sand: makeTex(ctx => {
noisePixels(ctx, [218,205,145], 15);
}),
water: makeTex(ctx => {
noisePixels(ctx, [60,100,200], 15);
}),
woodTop: makeTex(ctx => {
noisePixels(ctx, [165,130,75], 10);
// 年轮
ctx.strokeStyle='rgb(130,95,50)'; ctx.lineWidth=1;
for(let r=2;r<8;r+=2){ctx.beginPath();ctx.arc(8,8,r,0,Math.PI*2);ctx.stroke()}
}),
woodSide: makeTex(ctx => {
noisePixels(ctx, [110,80,45], 15);
// 竖纹
for(let x=0;x<TS;x+=4){ctx.fillStyle='rgb(85,60,30)';ctx.fillRect(x,0,1,16)}
}),
planks: makeTex(ctx => {
noisePixels(ctx, [185,150,90], 12);
// 横向木板线
for(let y=0;y<TS;y+=4){ctx.fillStyle='rgb(140,110,60)';ctx.fillRect(0,y,16,1)}
}),
leaves: makeTex(ctx => {
noisePixels(ctx, [60,110,35], 30);
// 穿透孔 (模拟透明)
for(let i=0;i<8;i++){ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillRect(Math.random()*16|0,Math.random()*16|0,1,1)}
}),
rblock: makeTex(ctx => {
noisePixels(ctx, [200,30,25], 20);
// 结晶纹路
ctx.strokeStyle='rgb(255,80,60)'; ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(4,2);ctx.lineTo(8,8);ctx.lineTo(4,14);ctx.moveTo(12,2);ctx.lineTo(8,8);ctx.lineTo(12,14);ctx.stroke();
}),
lampOff: makeTex(ctx => {
noisePixels(ctx, [55,55,65], 10);
ctx.strokeStyle='rgb(80,80,90)'; ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(4,4);ctx.lineTo(12,12);ctx.moveTo(12,4);ctx.lineTo(4,12);ctx.stroke();
}),
lampOn: makeTex(ctx => {
noisePixels(ctx, [252,211,77], 20);
ctx.strokeStyle='rgb(255,255,200)'; ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(4,4);ctx.lineTo(12,12);ctx.moveTo(12,4);ctx.lineTo(4,12);ctx.stroke();
}),
leverSide: makeTex(ctx => {
noisePixels(ctx, [120,85,50], 15);
// 杆
ctx.fillStyle='rgb(160,120,70)';
ctx.fillRect(7,2,2,10);
// 红石头
ctx.fillStyle='rgb(232,50,41)';
ctx.fillRect(6,1,4,3);
}),
leverTop: makeTex(ctx => {
noisePixels(ctx, [120,85,50], 15);
ctx.fillStyle='rgb(232,50,41)';
ctx.fillRect(6,6,4,4);
}),
button: makeTex(ctx => {
noisePixels(ctx, [100,100,110], 10);
ctx.fillStyle='rgb(70,70,80)'; ctx.fillRect(5,5,6,6);
ctx.fillStyle='rgb(140,140,150)'; ctx.fillRect(6,6,4,4);
}),
torch: makeTex(ctx => {
noisePixels(ctx, [60,40,25], 10);
ctx.fillStyle='rgb(150,110,60)'; ctx.fillRect(7,4,2,10);
ctx.fillStyle='rgb(232,50,41)'; ctx.fillRect(6,2,4,4);
ctx.fillStyle='rgb(255,120,80)'; ctx.fillRect(7,3,2,1);
}),
repeater: makeTex(ctx => {
noisePixels(ctx, [120,80,30], 12);
ctx.fillStyle='rgb(200,160,60)'; ctx.fillRect(3,7,10,2);
// 箭头
ctx.fillStyle='rgb(255,200,80)';
ctx.beginPath();ctx.moveTo(11,8);ctx.lineTo(8,6);ctx.lineTo(8,10);ctx.fill();
ctx.fillStyle='rgb(80,60,20)'; ctx.fillRect(5,5,2,6);
}),
diode: makeTex(ctx => {
noisePixels(ctx, [40,40,50], 8);
ctx.fillStyle='rgb(200,200,80)';
ctx.beginPath();ctx.moveTo(4,4);ctx.lineTo(12,4);ctx.lineTo(8,12);ctx.fill();
ctx.strokeStyle='rgb(255,255,120)';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(8,4);ctx.lineTo(8,12);ctx.stroke();
}),
npn: makeTex(ctx => {
noisePixels(ctx, [40,40,50], 8);
ctx.strokeStyle='rgb(100,200,255)';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(3,8);ctx.lineTo(6,8);ctx.moveTo(6,4);ctx.lineTo(6,12);ctx.moveTo(6,8);ctx.lineTo(13,4);ctx.moveTo(6,8);ctx.lineTo(13,12);ctx.stroke();
ctx.fillStyle='rgb(100,200,255)';ctx.fillRect(12,3,2,2);ctx.fillRect(12,11,2,2);
ctx.fillStyle='rgb(255,255,255)';ctx.font='6px monospace';ctx.fillText('N',2,6);
}),
andGate: makeTex(ctx => {
noisePixels(ctx, [50,50,60], 8);
ctx.fillStyle='rgb(100,255,100)';
ctx.fillRect(3,5,5,6);
ctx.beginPath();ctx.arc(8,8,3,-1.5,1.5);ctx.fill();
ctx.fillStyle='rgb(255,255,255)';ctx.font='7px monospace';ctx.fillText('&',5,10);
}),
orGate: makeTex(ctx => {
noisePixels(ctx, [50,50,60], 8);
ctx.strokeStyle='rgb(255,200,50)';ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(3,4);ctx.quadraticCurveTo(6,8,3,12);ctx.moveTo(3,4);ctx.quadraticCurveTo(9,5,13,8);ctx.quadraticCurveTo(9,11,3,12);ctx.stroke();
ctx.fillStyle='rgb(255,255,255)';ctx.font='6px monospace';ctx.fillText('≥1',5,9);
}),
notGate: makeTex(ctx => {
noisePixels(ctx, [50,50,60], 8);
ctx.fillStyle='rgb(255,100,100)';
ctx.beginPath();ctx.moveTo(4,5);ctx.lineTo(4,11);ctx.lineTo(10,8);ctx.fill();
ctx.fillStyle='rgb(255,100,100)';ctx.beginPath();ctx.arc(12,8,1.5,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgb(255,255,255)';ctx.font='7px monospace';ctx.fillText('!',6,10);
}),
dust: makeTex(ctx => {
noisePixels(ctx, [40,35,35], 5);
ctx.fillStyle='rgb(200,30,25)';
ctx.fillRect(7,2,2,12); ctx.fillRect(2,7,12,2);
ctx.beginPath();ctx.arc(8,8,2.5,0,Math.PI*2);ctx.fill();
}),
capacitor: makeTex(ctx => {
noisePixels(ctx, [40,40,50], 5);
ctx.fillStyle='rgb(80,150,255)';ctx.fillRect(4,4,8,2);ctx.fillRect(4,10,8,2);
ctx.fillStyle='rgb(120,180,255)';ctx.fillRect(7,2,2,12);
ctx.fillStyle='rgb(255,255,255)';ctx.font='6px monospace';ctx.fillText('C',6,9);
}),
resistor: makeTex(ctx => {
noisePixels(ctx, [150,120,70], 10);
ctx.fillStyle='rgb(200,150,60)';ctx.fillRect(3,6,10,4);
ctx.fillStyle='rgb(80,40,20)';ctx.fillRect(5,6,1,4);ctx.fillRect(8,6,1,4);ctx.fillRect(10,6,1,4);
ctx.fillStyle='rgb(120,85,50)';ctx.fillRect(2,7,1,2);ctx.fillRect(13,7,1,2);
}),
led: makeTex(ctx => {
noisePixels(ctx, [30,30,35], 5);
ctx.fillStyle='rgb(50,255,50)';ctx.beginPath();ctx.arc(8,8,3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgb(150,255,150)';ctx.beginPath();ctx.arc(7,7,1,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgb(100,100,100)';ctx.fillRect(7,11,2,3);
}),
};
// 加载原版 PNG 材质(替换程序化纹理)
function loadTex(path) {
const tex = new THREE.TextureLoader().load(path);
tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter;
return tex;
}
TEX.grassTop = loadTex('textures/grass_block_top.png');
TEX.grassSide = loadTex('textures/grass_block_side.png');
TEX.dirt = loadTex('textures/dirt.png');
TEX.stone = loadTex('textures/stone.png');
TEX.sand = loadTex('textures/sand.png');
TEX.woodTop = loadTex('textures/oak_log_top.png');
TEX.woodSide = loadTex('textures/oak_log.png');
TEX.planks = loadTex('textures/oak_planks.png');
TEX.leaves = loadTex('textures/oak_leaves.png');
TEX.rblock = loadTex('textures/redstone_block.png');
TEX.lampOff = loadTex('textures/redstone_lamp.png');
TEX.lampOn = loadTex('textures/redstone_lamp_on.png');
TEX.torch = loadTex('textures/redstone_torch.png');
TEX.leverSide = loadTex('textures/lever.png');
TEX.leverTop = loadTex('textures/lever.png');
TEX.repeater = loadTex('textures/repeater.png');
TEX.dust = loadTex('textures/redstone_dust_dot.png');
// 快捷栏/背包图标用 PNG 路径
const PNG_ICONS = {
grass:'textures/grass_block_side.png', stone:'textures/stone.png', dirt:'textures/dirt.png',
sand:'textures/sand.png', wood:'textures/oak_log.png', planks:'textures/oak_planks.png',
leaves:'textures/oak_leaves.png', rblock:'textures/redstone_block.png', lever:'textures/lever.png',
button:'textures/lever.png', torch:'textures/redstone_torch.png', dust:'textures/redstone_dust_dot.png',
repeater:'textures/repeater.png', lamp:'textures/redstone_lamp.png',
};
// ==================== 方块定义 ====================
// faces: [right,left,top,bottom,front,back]
const BLOCKS = [
// 建筑材料
{ id:'grass', name:'草方块', cat:'建筑', faces:[TEX.grassSide,TEX.grassSide,TEX.grassTop,TEX.dirt,TEX.grassSide,TEX.grassSide] },
{ id:'stone', name:'石头', cat:'建筑', faces:[TEX.stone,TEX.stone,TEX.stone,TEX.stone,TEX.stone,TEX.stone] },
{ id:'dirt', name:'泥土', cat:'建筑', faces:[TEX.dirt,TEX.dirt,TEX.dirt,TEX.dirt,TEX.dirt,TEX.dirt] },
{ id:'sand', name:'沙子', cat:'建筑', faces:[TEX.sand,TEX.sand,TEX.sand,TEX.sand,TEX.sand,TEX.sand] },
{ id:'wood', name:'原木', cat:'建筑', faces:[TEX.woodSide,TEX.woodSide,TEX.woodTop,TEX.woodTop,TEX.woodSide,TEX.woodSide] },
{ id:'planks', name:'木板', cat:'建筑', faces:[TEX.planks,TEX.planks,TEX.planks,TEX.planks,TEX.planks,TEX.planks] },
{ id:'leaves', name:'树叶', cat:'建筑', faces:Array(6).fill(TEX.leaves), transparent:true },
// 电源
{ id:'rblock', name:'红石块', cat:'电源', faces:Array(6).fill(TEX.rblock), power:true },
{ id:'lever', name:'拉杆', cat:'电源', faces:[TEX.leverSide,TEX.leverSide,TEX.leverTop,TEX.dirt,TEX.leverSide,TEX.leverSide], isLever:true },
{ id:'button', name:'按钮', cat:'电源', faces:Array(6).fill(TEX.button), isButton:true },
{ id:'torch', name:'红石火把',cat:'电源', faces:Array(6).fill(TEX.torch), isTorch:true },
// 传输
{ id:'dust', name:'红石粉', cat:'传输', faces:Array(6).fill(TEX.dust), isDust:true },
{ id:'repeater',name:'中继器', cat:'传输', faces:Array(6).fill(TEX.repeater), isRepeater:true },
{ id:'diode', name:'二极管', cat:'传输', faces:Array(6).fill(TEX.diode), isDiode:true },
// 逻辑门
{ id:'andGate',name:'AND门', cat:'逻辑', faces:Array(6).fill(TEX.andGate), isGate:'AND' },
{ id:'orGate', name:'OR门', cat:'逻辑', faces:Array(6).fill(TEX.orGate), isGate:'OR' },
{ id:'notGate',name:'NOT门', cat:'逻辑', faces:Array(6).fill(TEX.notGate), isGate:'NOT' },
// 模拟元件
{ id:'npn', name:'NPN三极管',cat:'模拟', faces:Array(6).fill(TEX.npn), isNPN:true },
{ id:'capacitor',name:'电容', cat:'模拟', faces:Array(6).fill(TEX.capacitor) },
{ id:'resistor',name:'电阻', cat:'模拟', faces:Array(6).fill(TEX.resistor) },
// 输出
{ id:'lamp', name:'红石灯', cat:'输出', faces:Array(6).fill(TEX.lampOff), isLamp:true, litFaces:Array(6).fill(TEX.lampOn) },
{ id:'led', name:'LED', cat:'输出', faces:Array(6).fill(TEX.led), isLamp:true, litFaces:Array(6).fill(TEX.led) },
];
const HOTBAR_SIZE = 10; // 快捷栏显示前10个 (1-9 + 0)
// ==================== 方向性元件引脚定义 ====================
// 默认朝向: 输出=+Z (前方), 输入=-Z (后方)
// pin: [x, z, length, color] 引脚从元件体(±0.2)延伸到相邻方块表面(±0.5)
const PIN_LAYOUTS = {
diode: { pins: [[0,-0.35,0.3,0x6080a0],[0,0.35,0.3,0xe83229]] },
npn: { pins: [[0,-0.35,0.3,0x6080a0],[0.35,0,0.3,0xe83229],[-0.35,0,0.3,0x3FB950]] },
andGate: { pins: [[-0.35,0,0.3,0x6080a0],[0.35,0,0.3,0x6080a0],[0,0.35,0.3,0xe83229]] },
orGate: { pins: [[-0.35,0,0.3,0x6080a0],[0.35,0,0.3,0x6080a0],[0,0.35,0.3,0xe83229]] },
notGate: { pins: [[0,-0.35,0.3,0x6080a0],[0,0.35,0.3,0xe83229]] },
repeater: { pins: [[0,-0.35,0.3,0x6080a0],[0,0.35,0.3,0xe83229]] },
};
function isDirectional(typeId) {
return !!PIN_LAYOUTS[typeId];
}
// ==================== 全局状态 ====================
const WORLD_R = 30; // 世界半径 (用于初始生成)
const SEA_LEVEL = 1;
const GRAVITY = 28, JUMP = 9, SPEED = 5.5, P_H = 1.7, P_W = 0.6, REACH = 6;
const MAX_FALL = 30; // 最大下落速度,防止高速穿透方块嵌入卡死
const CHUNK_SIZE = 16; // 区块大小 16x16x16
const MIN_CHUNK_Y = Math.floor(-64 / CHUNK_SIZE); // -4
const MAX_CHUNK_Y = Math.floor(500 / CHUNK_SIZE); // 31
const RENDER_DIST = 4; // 玩家周围加载区块半径
const UNLOAD_DIST = 6; // 超出此距离卸载区块
let scene, camera, renderer, raycaster, highlight;
let world = new Map();
let chunkMeshes = new Map(); // "cx,cy,cz" → [THREE.Mesh]
let terrainColumns = {}; // "cx,cz" → true (地形已生成)
let generatedChunks = {}; // "cx,cy,cz" → true (mesh已构建)
let chunkBlockCount = {}; // "cx,cy,cz" → 区块内方块数 (用于空区块快速跳过)
let terrainNoise = null; // 延迟初始化
let selectedSlot = 0;
let pitch = 0, yaw = 0;
let keys = {};
let gameActive = false; // 是否处于游戏中(WebView2 中 pointer lock 可能失败,用此标志替代)
let dragLook = false; // 无 pointer lock 时的拖动视角模式
let player = { pos: new THREE.Vector3(0, 10, 0), vel: new THREE.Vector3(), onGround: false };
let lastT = performance.now(), frames = 0, fpsT = 0, fps = 0;
const bk = (x,y,z) => `${x},${y},${z}`;
const bp = k => k.split(',').map(Number);
// 破坏动画状态
let breakingBlock = null; // {x,y,z,progress,mesh}
let breakStartTime = 0;
const BREAK_TIME = 600; // ms 破坏时间
// 粒子系统
function spawnParticles(x, y, z, color) {
const worldPos = new THREE.Vector3(x+0.5, y+0.5, z+0.5);
const screenPos = worldPos.clone().project(camera);
const sx = (screenPos.x * 0.5 + 0.5) * window.innerWidth;
const sy = (-screenPos.y * 0.5 + 0.5) * window.innerHeight;
for (let i = 0; i < 8; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.cssText = `
left:${sx}px;top:${sy}px;
width:${2+Math.random()*3}px;height:${2+Math.random()*3}px;
background:${color};
opacity:1;
transition:all ${0.4+Math.random()*0.3}s ease-out;
`;
document.body.appendChild(p);
requestAnimationFrame(() => {
p.style.transform = `translate(${(Math.random()-0.5)*40}px,${(Math.random()-0.5)*40-10}px)`;
p.style.opacity = '0';
});
setTimeout(() => p.remove(), 800);
}
}
// 地图存档
const SAVE_KEY = 'redcircuit_world_save_v1';
function saveWorld() {
// 只保存玩家周围存档半径内的方块,避免无限地形下序列化整个 world
const px = Math.round(player.pos.x), pz = Math.round(player.pos.z);
const SAVE_R = 96; // 存档半径
const data = [];
for (const [k, b] of world) {
const [x, y, z] = bp(k);
if (Math.abs(x - px) > SAVE_R || Math.abs(z - pz) > SAVE_R) continue;
data.push({ x, y, z, type: b.type, lit: b.lit || false, direction: (b.mesh && b.mesh.userData) ? (b.mesh.userData.direction || 0) : 0 });
}
const saveData = {
version: 1,
playerPos: { x: player.pos.x, y: player.pos.y, z: player.pos.z },
blocks: data,
timestamp: Date.now(),
};
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
} catch(e) {
// localStorage 满时静默失败
}
}
function loadWorld() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
const data = JSON.parse(raw);
if (data.version !== 1) return false;
// 空存档(0 方块)视为无效:走地形生成,避免进入虚空
if (!Array.isArray(data.blocks) || data.blocks.length === 0) return false;
// 清除当前世界
for (const [k, b] of world) {
if (b.mesh) { scene.remove(b.mesh); b.mesh.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); }
}
world.clear();
// 清除旧区块网格
for (const meshes of chunkMeshes.values()) {
meshes.forEach(m => { scene.remove(m); if(m.geometry)m.geometry.dispose(); });
}
chunkMeshes.clear();
// 恢复方块
for (const bd of data.blocks) {
setBlockData(bd.x, bd.y, bd.z, bd.type);
const wb = getBlock(bd.x, bd.y, bd.z);
if (wb && bd.lit && wb.type === 'lever') {
wb.lit = true;
// 拉杆需要独立 mesh
const def = getDef(wb.type);
const mesh = makeBlockMesh(def);
mesh.position.set(bd.x+0.5, bd.y+0.5+(mesh.userData.yOffset||0), bd.z+0.5);
mesh.userData = { x:bd.x, y:bd.y, z:bd.z, type:bd.type, direction:bd.direction||0 };
wb.mesh = mesh;
const h = mesh.userData.handle;
if (h) h.rotation.x = -Math.PI/4;
scene.add(mesh);
}
}
buildAllChunks();
// 恢复玩家位置
if (data.playerPos) {
player.pos.set(data.playerPos.x, data.playerPos.y, data.playerPos.z);
}
simulate();
return true;
} catch(e) {
return false;
}
}
// ==================== 认证状态 ====================
const AUTH_URL = 'https://deaicup.com/redcircuit/api/auth';
const API_BASE = 'https://deaicup.com/redcircuit';
let authState = { token: null, user: null, loggedIn: false, offline: false };
// 面板切换
let currentPanel = 'welcome';
function showPanel(name) {
['welcome','login','register','hub','modes','shop','inventory','blueprint','pause'].forEach(id => {
document.getElementById('menu-'+id).style.display = 'none';
});
document.getElementById('menu-'+name).style.display = (name === 'hub' || name === 'modes' || name === 'shop' || name === 'inventory' || name === 'blueprint') ? 'flex' : 'flex';
currentPanel = name;
document.getElementById('loginError').textContent = '';
document.getElementById('regError').textContent = '';
if (name === 'login') document.getElementById('loginUser').focus();
if (name === 'register') document.getElementById('regUser').focus();
if (name === 'hub') renderHubPage();
if (name === 'shop') renderShop();
if (name === 'inventory') renderInventory();
if (name === 'blueprint') renderBlueprintMarket();
spawnMenuParticles();
}
// 菜单背景粒子
let menuParticleTimer = null;
function spawnMenuParticles() {
const container = document.getElementById('menuParticles');
container.innerHTML = '';
for (let i = 0; i < 30; i++) {
const p = document.createElement('div');
p.className = 'menu-particle';
const size = 1 + Math.random() * 2;
p.style.cssText = `
width:${size}px;height:${size}px;
left:${Math.random()*100}%;top:${Math.random()*100}%;
--dx:${(Math.random()-0.5)*120}px;--dy:${(Math.random()-0.5)*120-40}px;
animation-delay:${Math.random()*8}s;
animation-duration:${5+Math.random()*7}s;
`;
container.appendChild(p);
}
}
// API 调用
async function apiCall(method, path, body) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
if (authState.token) opts.headers['Authorization'] = 'Bearer ' + authState.token;
const base = (path.startsWith('/login') || path.startsWith('/register') || path.startsWith('/me'))
? AUTH_URL : API_BASE + '/api';
const res = await fetch(base + path, opts);
const data = await res.json();
if (!res.ok || data.code !== 0) throw new Error(data.message || '请求失败');
return data.data;
}
// 登录
async function doLogin() {
const user = document.getElementById('loginUser').value.trim();
const pass = document.getElementById('loginPass').value;
const errEl = document.getElementById('loginError');
const btn = document.getElementById('loginBtn');
errEl.textContent = '';
if (!user || !pass) { errEl.textContent = '请填写用户名和密码'; return; }
btn.textContent = '登录中...'; btn.disabled = true;
try {
const data = await apiCall('POST', '/login', { username: user, password: pass });
authState.token = data.token;
authState.user = data.user;
authState.loggedIn = true;
authState.offline = false;
showMainMenu();
} catch(e) {
errEl.textContent = e.message;
} finally {
btn.textContent = '登录'; btn.disabled = false;
}
}
// 注册
async function doRegister() {
const user = document.getElementById('regUser').value.trim();
const pass = document.getElementById('regPass').value;
const pass2 = document.getElementById('regPass2').value;
const errEl = document.getElementById('regError');
const btn = document.getElementById('regBtn');
errEl.textContent = '';
if (!user || !pass) { errEl.textContent = '请填写用户名和密码'; return; }
if (user.length < 3 || user.length > 20) { errEl.textContent = '用户名需3-20个字符'; return; }
if (pass.length < 6) { errEl.textContent = '密码至少6位'; return; }
if (pass !== pass2) { errEl.textContent = '两次密码不一致'; return; }
btn.textContent = '注册中...'; btn.disabled = true;
try {
const data = await apiCall('POST', '/register', { username: user, password: pass });
authState.token = data.token;
authState.user = data.user;
authState.loggedIn = true;
authState.offline = false;
showMainMenu();
} catch(e) {
errEl.textContent = e.message;
} finally {
btn.textContent = '注册'; btn.disabled = false;
}
}
// 退出登录
function doLogout() {
authState = { token: null, user: null, loggedIn: false, offline: false };
showPanel('welcome');
}
// 显示主菜单 (Hub 页面)
function showMainMenu() {
showPanel('hub');
}
// 渲染 Hub 页面 (头像、皮肤、货币)
function renderHubPage() {
const u = authState.user || {};
document.getElementById('hubNickname').textContent = u.username || '玩家';
document.getElementById('hubLevel').textContent = 'Lv.' + (u.level || 1);
document.getElementById('hubDiamonds').textContent = u.diamonds || 0;
document.getElementById('hubRSCoins').textContent = u.redstone_coins || 0;
document.getElementById('hubGoldCoins').textContent = u.gold_coins || 0;
document.getElementById('hubLogout').style.display = authState.loggedIn ? 'inline-block' : 'none';
document.getElementById('hubDailyBtn').style.display = authState.loggedIn ? 'inline-block' : 'none';
drawHubAvatar(u.username || '玩家');
// 检查今日是否已签到
if (authState.loggedIn) {
const today = new Date().toISOString().slice(0, 10);
const btn = document.getElementById('hubDailyBtn');
if (u.last_login_date === today) {
btn.textContent = '✅ 已签到';
btn.style.color = '#888';
btn.style.pointerEvents = 'none';
} else {
btn.textContent = '🎁 签到';
btn.style.color = '#58A6FF';
btn.style.pointerEvents = 'auto';
}
}
}
// 每日签到
async function claimDailyReward() {
const btn = document.getElementById('hubDailyBtn');
btn.textContent = '领取中...'; btn.disabled = true;
try {
const data = await apiCall('POST', '/daily-reward');
if (data.claimed) {
showToast('今日已签到!');
btn.textContent = '✅ 已签到';
btn.style.color = '#888';
btn.style.pointerEvents = 'none';
} else {
showToast(`签到成功! +${data.diamonds}💎 +${data.redstone}石 +${data.gold}金`);
authState.user.diamonds = data.total_diamonds;
authState.user.redstone_coins = data.total_redstone;
authState.user.gold_coins = data.total_gold;
authState.user.last_login_date = new Date().toISOString().slice(0, 10);
renderHubPage();
btn.textContent = '✅ 已签到';
btn.style.color = '#888';
btn.style.pointerEvents = 'none';
}
} catch(e) {
showToast('签到失败: ' + e.message);
btn.textContent = '🎁 签到';
}
btn.disabled = false;
}
// ==================== 商店系统 ====================
const SHOP_ITEMS = [
{ id:'diamond_10', name:'钻石礼包', desc:'10 钻石', icon:'💎', price:5000, currency:'redstone_coins', priceLabel:'5000 红石币' },
{ id:'diamond_50', name:'钻石大礼包', desc:'50 钻石', icon:'💠', price:1, currency:'gold_coins', priceLabel:'1 金币' },
{ id:'gold_5', name:'金币兑换', desc:'5 金币', icon:'🟡', price:50, currency:'diamonds', priceLabel:'50 钻石' },
{ id:'rs_boost', name:'红石币加成', desc:'+10000 红石币', icon:'🪙', price:10, currency:'diamonds', priceLabel:'10 钻石' },
{ id:'skin_random', name:'随机皮肤', desc:'随机解锁一款皮肤', icon:'🎨', price:100, currency:'diamonds', priceLabel:'100 钻石' },
{ id:'level_boost', name:'等级加速', desc:'+500 经验值', icon:'⚡', price:30, currency:'diamonds', priceLabel:'30 钻石' },
];
function renderShop() {
const u = authState.user || {};
document.getElementById('shopBalance').innerHTML =
`余额: 💎${u.diamonds||0} | 🪙${u.redstone_coins||0} | 🟡${u.gold_coins||0}`;
const grid = document.getElementById('shopGrid');
grid.innerHTML = '';
SHOP_ITEMS.forEach(item => {
const div = document.createElement('div');
div.className = 'shop-item';
const cc = item.currency === 'diamonds' ? 'diamonds' : item.currency === 'gold_coins' ? 'gold' : 'redstone';
div.innerHTML = `
<div class="shop-item-icon">${item.icon}</div>
<div class="shop-item-name">${item.name}</div>
<div class="shop-item-desc">${item.desc}</div>
<div class="shop-item-price ${cc}">${item.priceLabel}</div>
<button class="shop-buy-btn" onclick="buyShopItem('${item.id}')">购买</button>
`;
grid.appendChild(div);
});
}
async function buyShopItem(itemId) {
const item = SHOP_ITEMS.find(i => i.id === itemId);
if (!item) return;
if (!authState.loggedIn) { showToast('请先登录'); return; }
try {
const data = await apiCall('POST', '/buy', { item_id: item.id, item_name: item.name, currency: item.currency, amount: item.price, quantity: 1 });
authState.user = data.user;
showToast(`购买成功! ${item.name}已到账`);
renderShop();
if (currentPanel === 'hub') renderHubPage();
} catch(e) {
showToast('购买失败: ' + e.message);
}
}
// 背包渲染
function renderInventory() {
const u = authState.user || {};
const inv = u.inventory || {};
const grid = document.getElementById('invGrid');
const ITEM_DEFS = {
speed_boost: { icon:'⚡', name:'速度加成', desc:'移动速度+50%(1h)' },
exp_boost: { icon:'⭐', name:'经验加成', desc:'+500经验值' },
diamond_pack: { icon:'💎', name:'钻石包', desc:'+10钻石' },
gold_pack: { icon:'🟡', name:'金币包', desc:'+5金币' },
rs_pack: { icon:'🪙', name:'红石币包', desc:'+10000红石币' },
skin_random: { icon:'🎨', name:'随机皮肤', desc:'解锁新外观' },
level_boost: { icon:'📈', name:'等级加速', desc:'+500经验' },
};
const entries = Object.entries(inv);
if (!entries.length) { grid.innerHTML = '<div style="color:#666;grid-column:1/-1;text-align:center;padding:20px">背包为空,去商店购买道具吧!</div>'; }
else {
grid.innerHTML = entries.map(([id,count]) => {
const def = ITEM_DEFS[id] || { icon:'📦', name:id, desc:'' };
return `<div class="inv-item">
<div class="inv-item-icon">${def.icon}</div>
<div class="inv-item-name">${def.name}</div>
<div class="inv-item-count">x${count}</div>
<button class="inv-use-btn" onclick="useInventoryItem('${id}')">使用</button>
</div>`;
}).join('');
}
// 购买历史
const hist = u.purchase_history || [];
document.getElementById('invHistory').innerHTML = '<div class="inv-history-title">最近购买</div>' +
(hist.length ? hist.slice(0,10).map(h =>
`<div class="inv-history-item"><span>${h.item_name}</span><span style="color:#888">${new Date(h.timestamp*1000).toLocaleDateString()}</span></div>`
).join('') : '<div style="color:#666">暂无购买记录</div>');
}
async function useInventoryItem(itemId) {
if (!authState.loggedIn) { showToast('请先登录'); return; }
try {
const data = await apiCall('POST', '/use-item', { item_id: itemId });
authState.user = data.user;
showToast(data.message);
renderInventory();
if (currentPanel === 'hub') renderHubPage();
} catch(e) {
showToast('使用失败: ' + e.message);
}
}
// ==================== 蓝图市场 ====================
const BP_MOCK = [
{ id:'bp1', name:'4位加法器', desc:'经典红石加法器电路', author:'Redstoner99', stars:4.5, downloads:230, price:0, cat:'logic' },
{ id:'bp2', name:'可调脉冲时钟', desc:'1Hz~20Hz 可调红石时钟', author:'ClockMaster', stars:4.8, downloads:567, price:100, cat:'clock', currency:'redstone_coins' },
{ id:'bp3', name:'D触发器', desc:'边沿触发D型锁存器', author:'LogicPro', stars:4.2, downloads:189, price:0, cat:'memory' },
{ id:'bp4', name:'8位ALU', desc:'简易算术逻辑单元', author:'CPUBuilder', stars:4.9, downloads:1023, price:500, cat:'cpu', currency:'redstone_coins' },
{ id:'bp5', name:'上升沿检测器', desc:'检测信号上升沿', author:'Redstoner99', stars:4.1, downloads:89, price:0, cat:'logic' },
{ id:'bp6', name:'3位计数器', desc:'二进制递增计数器', author:'CounterKing', stars:4.3, downloads:312, price:200, cat:'memory', currency:'redstone_coins' },
];
let bpTab = 'market';
function switchBPTab(el, tab) { bpTab = tab; document.querySelectorAll('.bp-tab').forEach(t => t.classList.remove('active')); el.classList.add('active'); document.getElementById('bpUploadForm').style.display = tab === 'upload' ? 'flex' : 'none'; renderBlueprintMarket(); }
function renderBlueprintMarket() {
const search = (document.getElementById('bpSearch')?.value || '').toLowerCase();
const cat = document.getElementById('bpCategory')?.value || 'all';
let items = BP_MOCK;
if (bpTab === 'mine') items = BP_MOCK.filter(b => b.author === 'Redstoner99');
if (cat !== 'all') items = items.filter(b => b.cat === cat);
if (search) items = items.filter(b => b.name.toLowerCase().includes(search) || b.desc.toLowerCase().includes(search));
document.getElementById('bpGrid').innerHTML = items.map(b => `
<div class="bp-card">
<div class="bp-card-name">${b.name}</div>
<div class="bp-card-desc">${b.desc}</div>
<div class="bp-card-meta">
<span class="bp-card-stars">${'★'.repeat(Math.round(b.stars))} ${b.stars}</span>
<span class="bp-card-price">${b.price ? b.price + ' 🪙' : '免费'}</span>
<button class="bp-card-btn" onclick="downloadBP('${b.id}')">${b.price ? '购买' : '下载'}</button>
</div>
</div>`).join('');
}
function downloadBP(id) {
const bp = BP_MOCK.find(b => b.id === id);
if (!bp) return;
if (bp.price && !authState.loggedIn) { showToast('请先登录'); return; }
showToast(`${bp.name} 下载成功! 已加入我的蓝图`);
}
function uploadBlueprint() {
if (!authState.loggedIn) { showToast('请先登录'); return; }
const name = document.getElementById('bpUploadName').value.trim();
if (!name) { showToast('请输入蓝图名称'); return; }
showToast('蓝图发布成功! (已扣除50💎)');
document.getElementById('bpUploadName').value = '';
document.getElementById('bpUploadDesc').value = '';
}
// 搜索输入监听
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const el = document.getElementById('bpSearch');
if (el) el.addEventListener('input', renderBlueprintMarket);
}, 500);
});
// 绘制头像
function drawHubAvatar(name) {
const c = document.getElementById('hubAvatar');
const ctx = c.getContext('2d');
const hash = name.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
const hue = hash % 360;
c.style.background = 'hsl(' + hue + ', 60%, 35%)';
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(name.charAt(0).toUpperCase(), 28, 28);
}
// 游戏模式选择
let selectedGameMode = 'creative';
let currentGameMode = 'creative'; // 当前激活的模式
let puzzleObjective = null; // 解密目标 {type, count, completed}
let timeAttackStart = 0;
let timeAttackDuration = 180; // 3分钟
function showGameModes() { showPanel('modes'); }
function selectMode(el, mode) {
document.querySelectorAll('.mode-card').forEach(c => c.classList.remove('active'));
el.classList.add('active');
selectedGameMode = mode;
}
function enterSelectedMode() {
currentGameMode = selectedGameMode;
if (currentGameMode === 'blueprint') {
document.getElementById('overlay').style.display = 'flex';
showPanel('blueprint');
return;
}
// 直接进入 HTML5 世界
document.getElementById('overlay').style.display = 'none';
if (currentGameMode === 'puzzle') { setupPuzzleMode(); }
if (currentGameMode === 'multiplayer') { setupTimeAttackMode(); }
startGame();
}
// 更新游戏内模式 HUD
function updateModeHUD() {
const badge = document.getElementById('modeHUD-badge');
const obj = document.getElementById('modeHUD-objective');
const timer = document.getElementById('modeHUD-timer');
const stars = document.getElementById('modeHUD-stars');
const score = document.getElementById('modeHUD-score');
badge.style.display = 'block'; obj.style.display = 'none';
timer.style.display = 'none'; stars.style.display = 'none'; score.style.display = 'none';
switch (currentGameMode) {
case 'creative':
badge.textContent = '🧱 自由搭建';
badge.style.background = 'rgba(88,166,255,0.9)';
obj.style.display = 'block';
obj.textContent = '无限资源 · 随心创造';
break;
case 'puzzle':
badge.textContent = '🧩 解密闯关';
badge.style.background = 'rgba(232,50,41,0.9)';
obj.style.display = 'block';
obj.innerHTML = puzzleObjective ? '目标: 点亮红石灯 💡' + (puzzleObjective.completed ? ' ✅ 完成!' : '') : '生成关卡中...';
stars.style.display = 'flex';
stars.innerHTML = ['★','★','★'].map((s,i) =>
`<span class="mode-hud-star${puzzleObjective?.completed ? ' earned' : ''}">${s}</span>`
).join('');
if (puzzleObjective?.completed) {
score.style.display = 'block';
score.textContent = '🌟 +50 红石币';
}
break;
case 'multiplayer':
badge.textContent = '⏱ 限时建造';
badge.style.background = 'rgba(88,166,255,0.85)';
obj.style.display = 'block';
obj.textContent = '3分钟内自由搭建!';
timer.style.display = 'block';
score.style.display = 'block';
break;
}
}
// 解密模式: 生成目标
function setupPuzzleMode() {
// 重置世界并生成解密关卡
for (const meshes of chunkMeshes.values()) {
meshes.forEach(m => { scene.remove(m); if(m.geometry)m.geometry.dispose(); });
}
chunkMeshes.clear();
for (const [k, b] of world) {
if (b.mesh) { scene.remove(b.mesh); b.mesh.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); }
}
world.clear();
// 生成小型解密地图: 5x5平台 + 预置电路元件
for (let x = -3; x <= 3; x++) for (let z = -3; z <= 3; z++) { setBlockData(x, 0, z, 'stone'); setBlockData(x, 1, z, 'stone'); }
// 预置: 红石块 + 红石粉 + 红石灯
placeBlock(-2, 2, 0, 'rblock');
placeBlock(0, 2, 0, 'dust');
placeBlock(1, 2, 0, 'dust');
placeBlock(2, 2, 0, 'lamp');
// 目标: 点亮红石灯
puzzleObjective = { type: 'power_lamp', pos: [2, 2, 0], completed: false };
buildAllChunks();
}
// 限时模式
function setupTimeAttackMode() {
timeAttackStart = Date.now();
puzzleObjective = { type: 'time_attack', score: 0, completed: false };
}
// 检查模式目标
function checkModeObjective() {
if (currentGameMode === 'puzzle' && puzzleObjective) {
const b = getBlock(puzzleObjective.pos[0], puzzleObjective.pos[1], puzzleObjective.pos[2]);
if (b && b.signal > 0 && !puzzleObjective.completed) {
puzzleObjective.completed = true;
showToast('解密成功! 红石灯已点亮 🎉');
}
}
}
// 开始游戏
function startGame() {
if (!window._worldLoaded) {
startWorld();
return;
}
ensurePlayerFree();
gameActive = true;
document.getElementById('overlay').style.display = 'none';
document.getElementById('menuParticles').innerHTML = '';
aiChatOpen = false; releaseReason = null;
document.getElementById('aiChat').classList.remove('show');
document.getElementById('aiBtn').classList.add('show');
document.getElementById('aiInput').value = '';
document.getElementById('modeHUD').style.display = 'flex';
updateModeHUD();
renderer.domElement.requestPointerLock();
}
// 恢复游戏 (从暂停)
function resumeGame() {
ensurePlayerFree();
gameActive = true;
document.getElementById('overlay').style.display = 'none';
document.getElementById('menuParticles').innerHTML = '';
aiChatOpen = false; releaseReason = null;
document.getElementById('aiChat').classList.remove('show');
document.getElementById('aiBtn').classList.add('show');
document.getElementById('aiInput').value = '';
document.getElementById('modeHUD').style.display = 'flex';
renderer.domElement.requestPointerLock();
}
// 检查认证服务连接
async function checkAuthService() {
try {
const res = await fetch(API_BASE + '/health');
if (res.ok) {
document.getElementById('menuStatus').textContent = 'v0.4 · 服务器已连接';
document.getElementById('menuStatus').style.color = '#58A6FF';
}
} catch(e) {
document.getElementById('menuStatus').textContent = 'v0.4 · 离线模式';
document.getElementById('menuStatus').style.color = '#888';
}
}
checkAuthService();
// ==================== AI 聊天状态 ====================
let aiChatOpen = false;
let releaseReason = null; // null | 'ai' | 'f11' | 'inventory'
function openAIChat() {
aiChatOpen = true;
gameActive = false;
document.getElementById('aiChat').classList.add('show');
// 清除键盘状态,防止卡键 (WASD 等)
keys = {};
if (document.pointerLockElement) {
releaseReason = 'ai';
document.exitPointerLock();
}
setTimeout(() => document.getElementById('aiInput').focus(), 100);
}
function closeAIChat() {
aiChatOpen = false;
document.getElementById('aiChat').classList.remove('show');
document.getElementById('aiInput').value = '';
if (releaseReason === 'ai') releaseReason = null;
if (!document.getElementById('inventory').classList.contains('show') && releaseReason === null) {
gameActive = true;
renderer.domElement.requestPointerLock();
}
}
function toggleAIChat() {
if (aiChatOpen) closeAIChat();
else openAIChat();
}
function sendAIMessage() {
const input = document.getElementById('aiInput');
const text = input.value.trim();
if (!text) return;
addChatMsg('user', '你', text);
input.value = '';
// 检查是否为地图命令
if (text.startsWith('/')) {
const result = execMapCommand(text);
setTimeout(() => addChatMsg('ai', 'AI 助手', result), 300);
return;
}
const reply = getAIReply(text);
setTimeout(() => addChatMsg('ai', 'AI 助手', reply), 400);
}
function addChatMsg(type, label, text) {
const container = document.getElementById('aiMessages');
const div = document.createElement('div');
div.className = 'ai-msg ' + type;
div.innerHTML = '<span class="label">' + label + '</span>' + text.replace(/\n/g, '<br>');
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function getAIReply(q) {
const lower = q.toLowerCase();
if (lower.includes('and') && (lower.includes('门') || lower.includes('gate'))) {
return 'AND 门需要两个输入都为 ON 时输出才为 ON。<br><br>搭建方法:<br>1. 放一个红石火把 → 连接两个输入线<br>2. 两个输入都通电时,火把熄灭 → 后面接 NOT 门反转<br>3. 简单方案:直接使用 AND 门方块!<br><br>需要我帮你在世界中放置吗?';
}
if (lower.includes('or') && (lower.includes('门') || lower.includes('gate'))) {
return 'OR 门:任一输入为 ON 时输出即为 ON。<br><br>用红石粉直接并联即可:<br>1. 两条输入线汇入同一条红石粉线路<br>2. 或者直接使用 OR 门方块<br><br>红石粉天然支持 OR 逻辑!';
}
if (lower.includes('not') && (lower.includes('门') || lower.includes('gate'))) {
return 'NOT 门(反相器):输入 ON → 输出 OFF,输入 OFF → 输出 ON。<br><br>用红石火把实现:<br>1. 在方块侧面放红石火把<br>2. 给该方块通电 → 火把熄灭<br>3. 火把输出端就是反相后的信号<br><br>也可以直接用 NOT 门方块!';
}
if (lower.includes('三极管') || lower.includes('npn')) {
return 'NPN 三极管:基极(B)通电时,集电极(C)的电流可以流到发射极(E)。<br><br>引脚方向:<br>- 后方 = 基极(控制端)<br>- 左方 = 集电极(输入端)<br>- 右方 = 发射极(输出端)<br><br>相当于一个电子开关!';
}
if (lower.includes('时钟') || lower.includes('脉冲') || lower.includes('clock')) {
return '红石时钟(脉冲发生器):<br><br>方案1:火把时钟<br>- 放两个红石火把,面对面通过红石粉连接<br>- 调整中继器延迟控制频率<br><br>方案2:中继器环形时钟<br>- 2-4个中继器首尾相连成环<br>- 右键调整各中继器延迟';
}
if (lower.includes('你好') || lower.includes('hi') || lower.includes('hello')) {
return '你好!我是 RedCircuit AI 助手。<br>我可以帮你:<br>- 解答电路原理问题<br>- 推荐搭建方案<br>- 解释各元件功能<br><br>随便问吧!';
}
return '这是一个好问题!我目前能回答关于:<br>- 逻辑门(AND/OR/NOT<br>- 红石基础电路<br>- 各元件使用方法<br>- NPN 三极管<br>- 时钟/脉冲电路<br><br>请试试问我这些问题!';
}
// ==================== AI 地图命令系统 ====================
function execMapCommand(cmd) {
const parts = cmd.slice(1).split(/\s+/);
const action = parts[0].toLowerCase();
// /tp x y z — 传送玩家
if (action === 'tp' && parts.length >= 4) {
const nx = parseFloat(parts[1]), ny = parseFloat(parts[2]), nz = parseFloat(parts[3]);
if (isNaN(nx) || isNaN(ny) || isNaN(nz)) return '❌ 坐标无效。用法:/tp x y z';
player.pos.set(nx, ny, nz);
player.vel.set(0, 0, 0);
updateCamera();
return `已传送至 (${nx.toFixed(1)}, ${ny.toFixed(1)}, ${nz.toFixed(1)})`;
}
if (action === 'tp' && parts.length === 2) {
if (parts[1] === 'spawn' || parts[1] === '重生点') {
player.pos.set(0, 10, 0);
player.vel.set(0, 0, 0);
updateCamera();
return '已传送至出生点 (0, 10, 0)';
}
if (parts[1] === 'here' || parts[1] === '当前位置') {
const p = player.pos;
return `当前位置:(${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})`;
}
return '❌ 未知目标。用法:/tp x y z | /tp spawn | /tp here';
}
// /setblock x y z type — 放置方块
if (action === 'setblock' && parts.length >= 5) {
const x = parseInt(parts[1]), y = parseInt(parts[2]), z = parseInt(parts[3]);
const type = parts.slice(4).join('_').toLowerCase();
if (isNaN(x) || isNaN(y) || isNaN(z)) return '❌ 坐标无效。用法:/setblock x y z 方块名';
const def = BLOCKS.find(b => b.id === type || b.name === type || b.id.includes(type));
if (!def) return '❌ 未知方块:' + type + '<br>可用:' + BLOCKS.map(b => b.id).join(', ');
if (getBlock(x, y, z)) return '❌ 位置 (' + x + ',' + y + ',' + z + ') 已有方块';
const ok = placeBlock(x, y, z, def.id);
return ok ? '已在 (' + x + ',' + y + ',' + z + ') 放置 ' + def.name : '❌ 放置失败';
}
// /setblock ~ ~ ~ type — 在玩家位置放置
if (action === 'setblock' && parts[1] === '~' && parts.length >= 5) {
const x = Math.round(player.pos.x), y = Math.round(player.pos.y), z = Math.round(player.pos.z);
const type = parts.slice(4).join('_').toLowerCase();
const def = BLOCKS.find(b => b.id === type || b.name === type || b.id.includes(type));
if (!def) return '❌ 未知方块:' + type;
if (getBlock(x, y, z)) return '❌ 脚下已有方块';
const ok = placeBlock(x, y, z, def.id);
return ok ? '已在脚下放置 ' + def.name : '❌ 放置失败';
}
// /fill x1 y1 z1 x2 y2 z2 type — 填充区域
if (action === 'fill' && parts.length >= 8) {
const x1 = parseInt(parts[1]), y1 = parseInt(parts[2]), z1 = parseInt(parts[3]);
const x2 = parseInt(parts[4]), y2 = parseInt(parts[5]), z2 = parseInt(parts[6]);
const type = parts.slice(7).join('_').toLowerCase();
if (isNaN(x1) || isNaN(y1) || isNaN(z1) || isNaN(x2) || isNaN(y2) || isNaN(z2)) {
return '❌ 坐标无效。用法:/fill x1 y1 z1 x2 y2 z2 方块名';
}
const def = BLOCKS.find(b => b.id === type || b.name === type || b.id.includes(type));
if (!def) return '❌ 未知方块:' + type;
const dx = Math.abs(x2 - x1) + 1, dy = Math.abs(y2 - y1) + 1, dz = Math.abs(z2 - z1) + 1;
const total = dx * dy * dz;
if (total > 500) return '❌ 填充区域太大(' + total + ' 方块),最多 500 个';
let count = 0;
const sx = Math.min(x1, x2), ex = Math.max(x1, x2);
const sy = Math.min(y1, y2), ey = Math.max(y1, y2);
const sz = Math.min(z1, z2), ez = Math.max(z1, z2);
for (let ix = sx; ix <= ex; ix++)
for (let iy = sy; iy <= ey; iy++)
for (let iz = sz; iz <= ez; iz++)
if (placeBlock(ix, iy, iz, def.id, true)) count++;
simulate(); // 放置完后统一仿真
return '已填充 ' + count + ' 个 ' + def.name;
}
// /cleararea x1 y1 z1 x2 y2 z2 — 清除区域
if ((action === 'cleararea' || action === 'clear') && parts.length >= 7) {
const x1 = parseInt(parts[1]), y1 = parseInt(parts[2]), z1 = parseInt(parts[3]);
const x2 = parseInt(parts[4]), y2 = parseInt(parts[5]), z2 = parseInt(parts[6]);
if (isNaN(x1) || isNaN(y1) || isNaN(z1) || isNaN(x2) || isNaN(y2) || isNaN(z2)) {
return '❌ 坐标无效。用法:/cleararea x1 y1 z1 x2 y2 z2';
}
const dx = Math.abs(x2 - x1) + 1, dy = Math.abs(y2 - y1) + 1, dz = Math.abs(z2 - z1) + 1;
const total = dx * dy * dz;
if (total > 500) return '❌ 清除区域太大(' + total + ' 方块),最多 500 个';
let count = 0;
const sx = Math.min(x1, x2), ex = Math.max(x1, x2);
const sy = Math.min(y1, y2), ey = Math.max(y1, y2);
const sz = Math.min(z1, z2), ez = Math.max(z1, z2);
for (let ix = sx; ix <= ex; ix++)
for (let iy = sy; iy <= ey; iy++)
for (let iz = sz; iz <= ez; iz++)
if (removeBlock(ix, iy, iz)) count++;
return '已清除 ' + count + ' 个方块';
}
// /help — 帮助
if (action === 'help') {
return '可用命令:<br>' +
'/tp x y z — 传送到坐标<br>' +
'/tp spawn — 回出生点<br>' +
'/tp here — 查看当前位置<br>' +
'/setblock x y z 方块名 — 放置方块<br>' +
'/fill x1 y1 z1 x2 y2 z2 方块名 — 填充区域 (≤500)<br>' +
'/cleararea x1 y1 z1 x2 y2 z2 — 清除区域 (≤500)<br>' +
'方块名可用:' + BLOCKS.map(b => b.id).slice(0, 10).join(', ') + ' ...';
}
return '❌ 未知命令:/' + action + '<br>输入 /help 查看可用命令';
}
// 背包数据模型 (Minecraft 风格: 可拖拽)
let hotbarSlots = []; // 快捷栏: BLOCKS 索引数组 (长度 HOTBAR_SIZE)
let invSlots = []; // 背包仓库: BLOCKS 索引数组
let carriedItem = null; // 鼠标拖拽中的物品 (BLOCKS 索引或 null)
// ==================== 方块网格创建 ====================
// ==================== 区块合批渲染 ====================
function getFaceVertices(x, y, z, fi) {
// fi: 0=right,1=left,2=top,3=bottom,4=front,5=back
const v = [
[[1,1,1, 1,0,1, 1,0,0, 1,1,0], [1,0,0, 1,0,0, 1,0,0, 1,0,0]], // right +X
[[0,1,0, 0,0,0, 0,0,1, 0,1,1], [-1,0,0, -1,0,0, -1,0,0, -1,0,0]], // left -X
[[1,1,0, 0,1,0, 0,1,1, 1,1,1], [0,1,0, 0,1,0, 0,1,0, 0,1,0]], // top +Y
[[0,0,1, 0,0,0, 1,0,0, 1,0,1], [0,-1,0, 0,-1,0, 0,-1,0, 0,-1,0]], // bottom -Y
[[1,1,1, 0,1,1, 0,0,1, 1,0,1], [0,0,1, 0,0,1, 0,0,1, 0,0,1]], // front +Z
[[0,1,0, 1,1,0, 1,0,0, 0,0,0], [0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1]], // back -Z
];
const [verts, norms] = v[fi];
const positions = [], normals = [];
for (let i = 0; i < 12; i += 3) {
positions.push(verts[i]+x, verts[i+1]+y, verts[i+2]+z);
normals.push(norms[i], norms[i+1], norms[i+2]);
}
return { positions, normals };
}
// 材质缓存:同纹理复用同一 Material,避免每区块重复创建
const sharedMats = new Map(); // tex → MeshLambertMaterial
function getBlockMaterial(tex) {
let m = sharedMats.get(tex);
if (!m) {
m = new THREE.MeshLambertMaterial({ map: tex });
sharedMats.set(tex, m);
}
return m;
}
function buildChunkMesh(cx, cy, cz) {
const ck = `${cx},${cy},${cz}`;
// 移除旧网格(材质为全局共享,仅 dispose 几何体)
if (chunkMeshes.has(ck)) {
chunkMeshes.get(ck).forEach(m => { scene.remove(m); if(m.geometry)m.geometry.dispose(); });
chunkMeshes.delete(ck);
}
// 空区块快速跳过(无方块无需构建)
if (!chunkBlockCount[ck]) return;
const x0 = cx * CHUNK_SIZE, y0 = cy * CHUNK_SIZE, z0 = cz * CHUNK_SIZE;
const facesByTex = {}; // key → { positions, normals, uvs, indices, tex }
var blockCount = 0, faceCount = 0;
for (let dx = 0; dx < CHUNK_SIZE; dx++) {
for (let dy = 0; dy < CHUNK_SIZE; dy++) {
for (let dz = 0; dz < CHUNK_SIZE; dz++) {
const x = x0 + dx, y = y0 + dy, z = z0 + dz;
const block = world.get(bk(x, y, z));
if (!block) continue;
blockCount++;
const def = getDef(block.type);
if (!def || !def.faces || def.isLever || def.isButton || def.isTorch) continue;
const neighbors = [
{ nx:1, ny:0, nz:0, fi:0 }, { nx:-1, ny:0, nz:0, fi:1 },
{ nx:0, ny:1, nz:0, fi:2 }, { nx:0, ny:-1, nz:0, fi:3 },
{ nx:0, ny:0, nz:1, fi:4 }, { nx:0, ny:0, nz:-1, fi:5 },
];
for (const n of neighbors) {
const nb = getBlock(x+n.nx, y+n.ny, z+n.nz);
const nbDef = nb ? getDef(nb.type) : null;
if (nb && (!nbDef || (!nbDef.transparent && !nbDef.isLever && !nbDef.isButton && !nbDef.isTorch))) continue;
const tex = def.faces[n.fi];
const texKey = n.fi + '_' + def.id;
if (!facesByTex[texKey]) facesByTex[texKey] = { positions:[], normals:[], uvs:[], indices:[], tex };
const ft = facesByTex[texKey];
const vi = ft.positions.length / 3;
const fv = getFaceVertices(x, y, z, n.fi);
ft.positions.push(...fv.positions);
ft.normals.push(...fv.normals);
ft.uvs.push(0,0, 1,0, 1,1, 0,1);
ft.indices.push(vi, vi+1, vi+2, vi, vi+2, vi+3);
faceCount++;
}
}
}
}
const meshes = [];
for (const ft of Object.values(facesByTex)) {
if (ft.positions.length === 0) continue;
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(ft.positions, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(ft.normals, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(ft.uvs, 2));
geo.setIndex(ft.indices);
const mesh = new THREE.Mesh(geo, getBlockMaterial(ft.tex));
scene.add(mesh);
meshes.push(mesh);
}
chunkMeshes.set(ck, meshes);
if (blockCount > 0) debugHUD('Chunk '+ck+': '+blockCount+' blocks, '+faceCount+' faces, '+meshes.length+' meshes');
}
function buildAllChunks() {
debugHUD('buildAllChunks...');
var count = 0;
for (let cx = Math.floor(-WORLD_R / CHUNK_SIZE); cx <= Math.floor((WORLD_R-1) / CHUNK_SIZE); cx++) {
for (let cy = MIN_CHUNK_Y; cy <= MAX_CHUNK_Y; cy++) {
for (let cz = Math.floor(-WORLD_R / CHUNK_SIZE); cz <= Math.floor((WORLD_R-1) / CHUNK_SIZE); cz++) {
buildChunkMesh(cx, cy, cz);
generatedChunks[cx+','+cy+','+cz] = true;
count++;
}
}
}
debugHUD('buildAllChunks done: '+count+' chunks, world='+world.size+' blocks');
}
// 无限地形:生成一个区块列的地形数据
function generateTerrainColumn(cx, cz) {
var key = cx + ',' + cz;
if (terrainColumns[key]) return;
terrainColumns[key] = true;
if (!terrainNoise) terrainNoise = new PerlinNoise(42);
var colCounts = {}; // 本列各区块计数 (cy → n)
var x0 = cx * CHUNK_SIZE, z0 = cz * CHUNK_SIZE;
for (var dx = 0; dx < CHUNK_SIZE; dx++) {
for (var dz = 0; dz < CHUNK_SIZE; dz++) {
var wx = x0 + dx, wz = z0 + dz;
var base = terrainNoise.octave(wx/24, wz/24, 4, 0.5);
var detail = terrainNoise.octave(wx/8, wz/8, 2, 0.3) * 0.3;
var h = Math.floor(base * 5 + detail * 3 + 3);
for (var y = 0; y <= h; y++) {
var type;
if (y === h) {
type = (h <= SEA_LEVEL) ? 'sand' : 'grass';
} else if (y >= h - 2) {
type = (h <= SEA_LEVEL + 1) ? 'sand' : 'dirt';
} else {
type = 'stone';
}
var k = bk(wx, y, wz);
if (!world.has(k)) world.set(k, { type: type, lit: false });
var cy = Math.floor(y / CHUNK_SIZE);
colCounts[cy] = (colCounts[cy] || 0) + 1;
}
if (h < SEA_LEVEL) {
for (var y = h + 1; y <= SEA_LEVEL; y++) {
var k = bk(wx, y, wz);
if (!world.has(k)) world.set(k, { type: 'sand', lit: false });
var cy = Math.floor(y / CHUNK_SIZE);
colCounts[cy] = (colCounts[cy] || 0) + 1;
}
}
}
}
// 合并到全局区块计数
for (var cy in colCounts) {
var ck = cx + ',' + cy + ',' + cz;
chunkBlockCount[ck] = (chunkBlockCount[ck] || 0) + colCounts[cy];
}
}
// 确保某区块有mesh
function ensureChunkMesh(cx, cy, cz) {
var key = cx + ',' + cy + ',' + cz;
if (generatedChunks[key]) return;
generatedChunks[key] = true;
// 空区块(无方块)跳过构建
if (!chunkBlockCount[key]) return;
buildChunkMesh(cx, cy, cz);
}
// 卸载一个区块
function unloadChunkMesh(key) {
if (chunkMeshes.has(key)) {
chunkMeshes.get(key).forEach(function(m) {
scene.remove(m);
if (m.geometry) m.geometry.dispose();
});
chunkMeshes.delete(key);
}
delete generatedChunks[key];
}
// 确保玩家周围的区块已加载
function ensureTerrainAround(px, pz) {
var ccx = Math.floor(px / CHUNK_SIZE);
var ccz = Math.floor(pz / CHUNK_SIZE);
// 生成渲染范围内的地形列和mesh
for (var cx = ccx - RENDER_DIST; cx <= ccx + RENDER_DIST; cx++) {
for (var cz = ccz - RENDER_DIST; cz <= ccz + RENDER_DIST; cz++) {
generateTerrainColumn(cx, cz);
for (var cy = MIN_CHUNK_Y; cy <= MAX_CHUNK_Y; cy++) {
ensureChunkMesh(cx, cy, cz);
}
}
}
// 卸载超出卸载距离的区块
var toRemove = [];
for (var key in generatedChunks) {
var parts = key.split(',').map(Number);
if (Math.abs(parts[0] - ccx) > UNLOAD_DIST || Math.abs(parts[2] - ccz) > UNLOAD_DIST) {
toRemove.push(key);
}
}
for (var i = 0; i < toRemove.length; i++) {
unloadChunkMesh(toRemove[i]);
}
}
// 旧函数保留兼容(用于startWorld初次加载)
function ensureInitialTerrain(px, pz) {
ensureTerrainAround(px, pz);
}
function refreshChunkAt(x, y, z) {
buildChunkMesh(Math.floor(x / CHUNK_SIZE), Math.floor(y / CHUNK_SIZE), Math.floor(z / CHUNK_SIZE));
}
// 设置方块数据 (不渲染)
function setBlockData(x, y, z, type) {
const k = bk(x, y, z);
if (world.has(k)) return false;
world.set(k, { type, lit: false });
bumpChunkCount(x, y, z, 1);
return true;
}
// 区块方块计数维护:用于空区块跳过渲染构建
function bumpChunkCount(x, y, z, delta) {
const ck = Math.floor(x / CHUNK_SIZE) + ',' + Math.floor(y / CHUNK_SIZE) + ',' + Math.floor(z / CHUNK_SIZE);
const n = (chunkBlockCount[ck] || 0) + delta;
if (n <= 0) delete chunkBlockCount[ck];
else chunkBlockCount[ck] = n;
}
function makeBlockMesh(blockDef) {
// 拉杆: 石质底座 + 可旋转木杆 (Group)
if (blockDef.isLever) {
const group = new THREE.Group();
// 底座: 石质平板
const baseGeo = new THREE.BoxGeometry(0.35, 0.12, 0.35);
const baseMat = new THREE.MeshLambertMaterial({ color: 0x808080, map: TEX.stone });
const base = new THREE.Mesh(baseGeo, baseMat);
base.position.y = -0.44;
group.add(base);
// 手柄: 木杆 (原点在底部,便于旋转)
const handleGeo = new THREE.BoxGeometry(0.06, 0.35, 0.06);
handleGeo.translate(0, 0.175, 0);
const handleMat = new THREE.MeshLambertMaterial({ color: 0x9c6b3c });
const handle = new THREE.Mesh(handleGeo, handleMat);
handle.position.y = -0.38;
group.add(handle);
group.userData.yOffset = 0;
group.userData.handle = handle;
group.userData.isLeverGroup = true;
return group;
}
let geo = new THREE.BoxGeometry(1, 1, 1);
let yOffset = 0;
if (blockDef.isButton) {
geo = new THREE.BoxGeometry(0.6, 0.3, 0.6); yOffset = -0.35;
} else if (blockDef.isTorch) {
geo = new THREE.BoxGeometry(0.2, 0.6, 0.2); yOffset = -0.2;
} else if (blockDef.isDust) {
geo = new THREE.BoxGeometry(0.9, 0.1, 0.9); yOffset = -0.45;
} else if (isDirectional(blockDef.id)) {
geo = new THREE.BoxGeometry(0.4, 0.15, 0.4); yOffset = -0.42;
}
const mats = blockDef.faces.map(tex => new THREE.MeshLambertMaterial({
map: tex,
transparent: blockDef.transparent || false,
alphaTest: blockDef.transparent ? 0.5 : 0,
}));
const mesh = new THREE.Mesh(geo, mats);
mesh.userData.yOffset = yOffset;
// 为方向性元件添加引脚
if (isDirectional(blockDef.id)) {
const layout = PIN_LAYOUTS[blockDef.id];
for (const [px, pz, len, color] of layout.pins) {
// 判断引脚方向: Z向还是X向
let pinGeo, pinPos;
if (pz !== 0) {
pinGeo = new THREE.BoxGeometry(0.05, 0.05, len);
pinPos = new THREE.Vector3(px, 0, pz);
} else {
pinGeo = new THREE.BoxGeometry(len, 0.05, 0.05);
pinPos = new THREE.Vector3(px, 0, pz);
}
const pinMat = new THREE.MeshLambertMaterial({ color: color, emissive: color, emissiveIntensity: 0.2 });
const pin = new THREE.Mesh(pinGeo, pinMat);
pin.position.copy(pinPos);
mesh.add(pin);
}
}
return mesh;
}
const DEF_MAP = new Map(BLOCKS.map(b => [b.id, b]));
function getDef(id) { return DEF_MAP.get(id); }
function placeBlock(x, y, z, typeId, skipUpdate, direction) {
const k = bk(x, y, z);
if (world.has(k)) return false;
const def = getDef(typeId);
// 有模型的元件 (拉杆/按钮/火把) 保留独立 mesh
if (def.isLever || def.isButton || def.isTorch) {
const mesh = makeBlockMesh(def);
const yOffset = mesh.userData.yOffset || 0;
mesh.position.set(x + 0.5, y + 0.5 + yOffset, z + 0.5);
let dir = 0;
if (isDirectional(typeId) && direction !== undefined) {
mesh.rotation.y = direction; dir = direction;
}
mesh.userData = { x, y, z, type: typeId, yOffset, direction: dir };
scene.add(mesh);
world.set(k, { type: typeId, mesh, lit: false });
bumpChunkCount(x, y, z, 1);
if (!skipUpdate) simulate();
return true;
}
// 普通方块: 存入数据 + 刷新区块
world.set(k, { type: typeId, lit: false });
bumpChunkCount(x, y, z, 1);
if (!skipUpdate) {
refreshChunkAt(x, y, z);
simulate();
}
return true;
}
function removeBlock(x, y, z) {
const k = bk(x, y, z);
const b = world.get(k);
if (!b) return false;
// 释放独立 mesh (如果有)
if (b.mesh) {
scene.remove(b.mesh);
b.mesh.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
else child.material.dispose();
}
});
}
world.delete(k);
bumpChunkCount(x, y, z, -1);
refreshChunkAt(x, y, z);
simulate();
return true;
}
function getBlock(x, y, z) { return world.get(bk(x, y, z)); }
// ==================== 电路仿真引擎 ====================
function getDirections(rot) {
const s = Math.round(Math.sin(rot)), c = Math.round(Math.cos(rot));
return {
forward: [s, c], backward: [-s, -c],
right: [c, -s], left: [-c, s],
};
}
function getNeighborSig(x, y, z, [dx, dz]) {
const n = getBlock(x+dx, y, z+dz);
return n ? (n.signal || 0) : 0;
}
function setNeighborSig(x, y, z, [dx, dz], val) {
const n = getBlock(x+dx, y, z+dz);
if (n && val > (n.signal || 0)) n.signal = val;
}
function bfsWires() {
const queue = [];
for (const [k, b] of world) {
if ((b.signal || 0) > 0) {
const def = getDef(b.type);
if (def.power || b.type === 'dust' || b.type === 'torch' || isDirectional(b.type) || b.type === 'lever' || b.type === 'button') {
queue.push({ k, sig: b.signal });
}
}
}
let changed = false;
while (queue.length) {
const { k, sig } = queue.shift();
if (sig <= 1) continue;
const [x, y, z] = bp(k);
for (const [dx, dz] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const n = getBlock(x+dx, y, z+dz);
if (!n || n.type !== 'dust') continue;
const ns = sig - 1;
if (ns > (n.signal || 0)) { n.signal = ns; changed = true; queue.push({ k: bk(x+dx,y,z+dz), sig: ns }); }
}
}
return changed;
}
function simulate() {
// 1. 重置信号
for (const [, b] of world) b.signal = 0;
// 2. 电源
for (const [, b] of world) {
const def = getDef(b.type);
if (def.power) b.signal = 15;
if (b.type === 'lever' && b.lit) b.signal = 15;
if (b.type === 'button' && b.lit) b.signal = 15;
}
// 3. 迭代传播 (最多20轮直到稳定)
for (let iter = 0; iter < 20; iter++) {
let changed = false;
// 3a. 导线BFS
changed = bfsWires() || changed;
// 3b. 火把反相
for (const [k, b] of world) {
if (b.type !== 'torch') continue;
const [x, y, z] = bp(k);
let adjPow = false;
for (const [dx, dy, dz] of [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]]) {
const n = getBlock(x+dx, y+dy, z+dz);
if (n && (n.signal||0) > 0 && n.type !== 'torch') { adjPow = true; break; }
}
const ns = adjPow ? 0 : 15;
if ((b.signal||0) !== ns) { b.signal = ns; changed = true; }
}
// 3c. 方向性元件
for (const [k, b] of world) {
if (!isDirectional(b.type) || !b.mesh) continue;
const [x, y, z] = bp(k);
const dir = getDirections(b.mesh.userData.direction || 0);
const def = getDef(b.type);
const inBack = getNeighborSig(x, y, z, dir.backward);
const inLeft = getNeighborSig(x, y, z, dir.left);
const inRight = getNeighborSig(x, y, z, dir.right);
let out = 0;
if (def.isDiode || def.isRepeater) {
out = inBack > 0 ? 15 : 0;
} else if (def.isGate === 'AND') {
out = (inLeft > 0 && inRight > 0) ? 15 : 0;
} else if (def.isGate === 'OR') {
out = (inLeft > 0 || inRight > 0) ? 15 : 0;
} else if (def.isGate === 'NOT') {
out = inBack > 0 ? 0 : 15;
} else if (def.isNPN) {
// 基极=后, 集电极=右, 发射极=左; 基极有信号时集电极->发射极导通
if (inBack > 0 && inRight > 0) setNeighborSig(x, y, z, dir.left, inRight);
continue;
}
if (out > 0) setNeighborSig(x, y, z, dir.forward, out);
}
// 3d. 再次导线BFS (传播门输出)
changed = bfsWires() || changed;
if (!changed) break;
}
// 4. 更新灯/LED
for (const [k, b] of world) {
const def = getDef(b.type);
if (!def.isLamp) continue;
const [x, y, z] = bp(k);
let powered = (b.signal||0) > 0;
if (!powered) {
for (const [dx, dy, dz] of [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]]) {
const n = getBlock(x+dx, y+dy, z+dz);
if (n && (n.signal||0) > 0) { powered = true; break; }
}
}
if (powered !== b.lit) {
b.lit = powered;
if (!b.mesh) { refreshChunkAt(x, y, z); continue; }
const nf = powered ? def.litFaces : def.faces;
const mats = Array.isArray(b.mesh.material) ? b.mesh.material : [b.mesh.material];
mats.forEach((m, i) => { if (nf[i]) { m.map = nf[i]; m.needsUpdate = true; } });
mats.forEach(m => {
m.emissive = powered ? new THREE.Color(0xfcd34d) : new THREE.Color(0x000000);
m.emissiveIntensity = powered ? 0.3 : 0;
});
}
}
}
// ==================== 地形生成 ====================
// generateTerrain 已替换为 generateTerrainColumn + ensureTerrainAround 无限地形系统
function generateTree(x, y, z) {
const trunkH = 4 + (Math.random() * 2 | 0);
// 树干
for (let i = 0; i < trunkH; i++) setBlockData(x, y + i, z, 'wood');
// 树冠: 3层
const top = y + trunkH;
// 底层 5x5
for (let dx = -2; dx <= 2; dx++)
for (let dz = -2; dz <= 2; dz++) {
if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && Math.random() < 0.5) continue;
setBlockData(x + dx, top - 1, z + dz, 'leaves');
}
// 中层 3x3
for (let dx = -1; dx <= 1; dx++)
for (let dz = -1; dz <= 1; dz++)
setBlockData(x + dx, top, z + dz, 'leaves');
// 顶层
setBlockData(x, top + 1, z, 'leaves');
if (Math.random() < 0.5) setBlockData(x + 1, top + 1, z, 'leaves');
if (Math.random() < 0.5) setBlockData(x, top + 1, z + 1, 'leaves');
}
// ==================== 快捷栏 & 背包 (Minecraft 风格) ====================
function makeSlotIcon(blockIdx) {
const b = BLOCKS[blockIdx];
if (!b) return document.createElement('div');
if (PNG_ICONS[b.id]) {
const img = document.createElement('img');
img.src = PNG_ICONS[b.id]; img.style.imageRendering = 'pixelated';
return img;
}
const c = document.createElement('canvas');
c.width = 32; c.height = 32; c.style.imageRendering = 'pixelated';
const ctx = c.getContext('2d'); ctx.imageSmoothingEnabled = false;
const ft = b.faces[2] || b.faces[0];
if (ft && ft.image) ctx.drawImage(ft.image, 0, 0, 16, 16, 0, 0, 32, 32);
return c;
}
function createHotbar() {
const bar = document.getElementById('hotbar');
bar.innerHTML = '';
for (let i = 0; i < HOTBAR_SIZE; i++) {
const slot = document.createElement('div');
slot.className = 'slot' + (i === selectedSlot ? ' active' : '');
slot.dataset.idx = i;
const bi = hotbarSlots[i];
if (bi !== undefined && bi !== null) {
const icon = makeSlotIcon(bi);
icon.className = 'slot-icon';
slot.appendChild(icon);
const name = document.createElement('span');
name.className = 'slot-name'; name.textContent = BLOCKS[bi].name;
slot.appendChild(name);
}
const key = document.createElement('span');
key.className = 'slot-key'; key.textContent = i === 9 ? 0 : i + 1;
slot.appendChild(key);
slot.title = (bi != null) ? BLOCKS[bi].name : '空';
slot.addEventListener('click', () => selectSlot(i));
bar.appendChild(slot);
}
}
function createInventory() {
// 左侧: 方块选取区 (按分类分组, 无限源)
const creative = document.getElementById('invCreative');
creative.innerHTML = '';
const cats = {};
BLOCKS.forEach((b, i) => { if (!cats[b.cat]) cats[b.cat] = []; cats[b.cat].push(i); });
for (const [cat, indices] of Object.entries(cats)) {
const label = document.createElement('div');
label.className = 'inv-cat-label'; label.textContent = cat;
creative.appendChild(label);
for (const i of indices) {
creative.appendChild(makeInvSlot(i, i, 'creative'));
}
}
// 右侧: 存储区 (18格) + 快捷栏 (10格)
const storage = document.getElementById('invStorage');
storage.innerHTML = '';
for (let i = 0; i < 27; i++) storage.appendChild(makeInvSlot(invSlots[i], i, 'storage'));
const hb = document.getElementById('invHotbar');
hb.innerHTML = '';
for (let i = 0; i < HOTBAR_SIZE; i++) {
const s = makeInvSlot(hotbarSlots[i], i, 'hotbar');
if (i === selectedSlot) s.classList.add('active');
hb.appendChild(s);
}
}
function makeInvSlot(blockIdx, slotIdx, source) {
const slot = document.createElement('div');
slot.className = 'inv-slot';
slot.dataset.source = source;
slot.dataset.slot = slotIdx;
if (blockIdx !== undefined && blockIdx !== null) {
slot.appendChild(makeSlotIcon(blockIdx));
slot.title = BLOCKS[blockIdx].name;
// 自定义悬停提示
slot.addEventListener('mouseenter', () => showInvTooltip(BLOCKS[blockIdx].name));
slot.addEventListener('mouseleave', () => hideInvTooltip());
}
if (source === 'hotbar') {
const n = document.createElement('span');
n.className = 'slot-num'; n.textContent = slotIdx === 9 ? 0 : slotIdx + 1;
slot.appendChild(n);
}
slot.addEventListener('mousedown', (e) => { e.preventDefault(); onSlotMousedown(source, slotIdx); });
return slot;
}
function onSlotMousedown(source, slotIdx) {
if (source === 'creative') {
// 创造模式: 无限源
if (carriedItem === null) {
carriedItem = slotIdx; // slotIdx = BLOCKS 索引, 拾取副本
} else {
carriedItem = null; // 丢弃手中物品
}
} else {
// 存储/快捷栏: 正常拾取/放置/交换
const arr = source === 'hotbar' ? hotbarSlots : invSlots;
const item = arr[slotIdx];
if (carriedItem === null) {
if (item != null) { carriedItem = item; arr[slotIdx] = null; }
} else {
if (item != null) { arr[slotIdx] = carriedItem; carriedItem = item; }
else { arr[slotIdx] = carriedItem; carriedItem = null; }
}
}
updateCarriedItem(); createInventory(); createHotbar();
}
function updateCarriedItem() {
const el = document.getElementById('carriedItem');
if (carriedItem === null) { el.style.display = 'none'; el.innerHTML = ''; }
else { el.style.display = 'block'; el.innerHTML = ''; el.appendChild(makeSlotIcon(carriedItem)); }
}
function showInvTooltip(name) {
const t = document.getElementById('invTooltip');
t.textContent = name; t.style.display = 'block';
}
function hideInvTooltip() {
document.getElementById('invTooltip').style.display = 'none';
}
function toggleInventory(force) {
const inv = document.getElementById('inventory');
const show = force !== undefined ? force : !inv.classList.contains('show');
inv.classList.toggle('show', show);
if (show) {
gameActive = false;
createInventory();
if (document.pointerLockElement) document.exitPointerLock();
} else {
// 关闭时归还拖拽中的物品到第一个空位
if (carriedItem !== null) {
const empty = invSlots.indexOf(null);
if (empty >= 0) invSlots[empty] = carriedItem;
else hotbarSlots[hotbarSlots.indexOf(null)] = carriedItem;
carriedItem = null; updateCarriedItem();
}
gameActive = true;
renderer.domElement.requestPointerLock();
}
}
function selectSlot(i) {
selectedSlot = i;
document.querySelectorAll('#hotbar .slot').forEach((s, idx) => s.classList.toggle('active', idx === i));
document.querySelectorAll('#inventory .inv-hotbar .inv-slot').forEach((s, idx) => s.classList.toggle('active', idx === i));
const bi = hotbarSlots[i];
if (bi != null) showToast(BLOCKS[bi].name);
}
// 初始化背包数据
function initInventoryData() {
hotbarSlots = BLOCKS.map((_, i) => i).slice(0, HOTBAR_SIZE);
invSlots = new Array(27).fill(null);
const remaining = BLOCKS.map((_, i) => i).slice(HOTBAR_SIZE);
for (let i = 0; i < remaining.length && i < 27; i++) invSlots[i] = remaining[i];
}
// ==================== 事件 ====================
function setupEvents() {
// 暂停面板按钮
document.getElementById('resumeBtn').addEventListener('click', resumeGame);
document.getElementById('menuBackBtn').addEventListener('click', () => {
gameActive = false;
document.getElementById('modeHUD').style.display = 'none';
document.getElementById('overlay').style.display = 'flex';
if (authState.loggedIn) showPanel('hub');
else showPanel('welcome');
});
// 登录/注册表单回车提交
document.getElementById('loginPass').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.getElementById('regPass2').addEventListener('keydown', e => { if (e.key === 'Enter') doRegister(); });
document.addEventListener('pointerlockchange', () => {
if (!document.pointerLockElement) {
if (document.getElementById('inventory').classList.contains('show')) return;
if (releaseReason === 'ai') return; // AI 聊天打开中
// F11 释放鼠标
if (releaseReason === 'f11') {
document.getElementById('overlay').style.display = 'none';
return;
}
// 正常暂停
if (gameActive) {
gameActive = false;
document.getElementById('overlay').style.display = 'flex';
showPanel('pause');
}
}
// 锁定鼠标后不再隐藏 AI 按钮,始终显示
});
document.addEventListener('mousemove', e => {
if (!gameActive) return;
// 游戏激活即转动视角:WebView2 中 pointer lock 可能失败,直接使用 movementX/Y
yaw -= (e.movementX || 0) * 0.0025;
pitch -= (e.movementY || 0) * 0.0025;
pitch = Math.max(-Math.PI/2+0.01, Math.min(Math.PI/2-0.01, pitch));
});
document.addEventListener('keydown', e => {
// F11: 释放/锁定鼠标 (不触发暂停)
if (e.code === 'F11') {
e.preventDefault();
if (document.pointerLockElement === renderer.domElement) {
releaseReason = 'f11';
document.exitPointerLock();
} else if (releaseReason === 'f11' && !aiChatOpen && !document.getElementById('inventory').classList.contains('show')) {
releaseReason = null;
renderer.domElement.requestPointerLock();
}
return;
}
// Enter: 打开/关闭 AI 聊天
if (e.code === 'Enter') {
if (aiChatOpen && document.activeElement === document.getElementById('aiInput')) {
// 输入框聚焦中 → 发送消息
e.preventDefault();
sendAIMessage();
return;
}
if (gameActive && !document.getElementById('inventory').classList.contains('show')) {
e.preventDefault();
toggleAIChat();
return;
}
}
// Escape: 关闭 AI 聊天 / 背包 / 暂停
if (e.code === 'Escape') {
if (aiChatOpen) { e.preventDefault(); closeAIChat(); return; }
if (document.getElementById('inventory').classList.contains('show')) { toggleInventory(false); return; }
// 游戏中按下 Esc 暂停(无 pointer lock 时同样生效)
if (gameActive) {
e.preventDefault();
gameActive = false;
if (document.pointerLockElement) document.exitPointerLock();
document.getElementById('overlay').style.display = 'flex';
showPanel('pause');
return;
}
}
if (e.code === 'KeyE') { e.preventDefault(); toggleInventory(); return; }
// 阻止浏览器默认行为 (防止空格滚动/WASD冲突)
if (gameActive) {
if (['Space','KeyW','KeyA','KeyS','KeyD'].includes(e.code)) e.preventDefault();
}
keys[e.code] = true;
const m = e.code.match(/Digit(\d)/);
if (m) { const d = parseInt(m[1]); const idx = d === 0 ? 9 : d - 1; if (idx < HOTBAR_SIZE) selectSlot(idx); }
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
document.addEventListener('mousedown', e => {
if (!gameActive) return;
if (document.pointerLockElement !== renderer.domElement) {
// 无 pointer lock: 按住拖动旋转视角
dragLook = true;
}
if (e.button === 0) breakBlock();
else if (e.button === 2) placeBlockAction();
});
document.addEventListener('mouseup', e => {
dragLook = false;
if (e.button === 0 && breakingBlock) finishBreak();
});
document.addEventListener('contextmenu', e => e.preventDefault());
// 拖拽物品 & 提示框跟随鼠标
document.addEventListener('mousemove', e => {
const el = document.getElementById('carriedItem');
if (el.style.display === 'block') {
el.style.left = (e.clientX - 18) + 'px';
el.style.top = (e.clientY - 18) + 'px';
}
const tt = document.getElementById('invTooltip');
if (tt.style.display === 'block') {
tt.style.left = (e.clientX + 14) + 'px';
tt.style.top = (e.clientY + 14) + 'px';
}
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
drawMCBackground();
});
}
// ==================== 方块交互 ====================
// DDA 体素光线步进:沿视线方向逐格检查,O(REACH) 而非遍历全 world
function getTarget() {
const origin = camera.position;
const dir = camera.getWorldDirection(new THREE.Vector3());
let x = Math.floor(origin.x), y = Math.floor(origin.y), z = Math.floor(origin.z);
const stepX = dir.x > 0 ? 1 : -1;
const stepY = dir.y > 0 ? 1 : -1;
const stepZ = dir.z > 0 ? 1 : -1;
const tDeltaX = Math.abs(1 / (dir.x || 1e-10));
const tDeltaY = Math.abs(1 / (dir.y || 1e-10));
const tDeltaZ = Math.abs(1 / (dir.z || 1e-10));
let tMaxX = stepX > 0 ? (x + 1 - origin.x) / dir.x : (origin.x - x) / -dir.x;
let tMaxY = stepY > 0 ? (y + 1 - origin.y) / dir.y : (origin.y - y) / -dir.y;
let tMaxZ = stepZ > 0 ? (z + 1 - origin.z) / dir.z : (origin.z - z) / -dir.z;
let normal = null;
let t = 0;
for (let i = 0; i < 256 && t < REACH; i++) {
const b = getBlock(x, y, z);
if (b) return { x, y, z, normal: normal || { x: 0, y: 1, z: 0 } };
if (tMaxX < tMaxY && tMaxX < tMaxZ) {
x += stepX; t = tMaxX; tMaxX += tDeltaX; normal = { x: -stepX, y: 0, z: 0 };
} else if (tMaxY < tMaxZ) {
y += stepY; t = tMaxY; tMaxY += tDeltaY; normal = { x: 0, y: -stepY, z: 0 };
} else {
z += stepZ; t = tMaxZ; tMaxZ += tDeltaZ; normal = { x: 0, y: 0, z: -stepZ };
}
}
return null;
}
function breakBlock() {
const t = getTarget();
if (!t) { breakingBlock = null; return; }
if (!breakingBlock || breakingBlock.x !== t.x || breakingBlock.y !== t.y || breakingBlock.z !== t.z) {
breakingBlock = { x: t.x, y: t.y, z: t.z, progress: 0 };
breakStartTime = performance.now();
}
}
function finishBreak() {
if (!breakingBlock) return;
const b = getBlock(breakingBlock.x, breakingBlock.y, breakingBlock.z);
if (!b) { breakingBlock = null; return; }
const def = getDef(b.type);
const color = def.faces ? '#aaa' : '#666';
spawnParticles(breakingBlock.x, breakingBlock.y, breakingBlock.z, b.type === 'stone' ? '#888' : b.type === 'dirt' ? '#a07040' : b.type === 'grass' ? '#7a8a4a' : '#ccc');
removeBlock(breakingBlock.x, breakingBlock.y, breakingBlock.z);
breakingBlock = null;
}
function placeBlockAction() {
const t = getTarget();
if (!t) return;
const b = getBlock(t.x, t.y, t.z);
// 右键交互: 拉杆切换开关
if (b && b.type === 'lever') {
b.lit = !b.lit;
const handle = b.mesh.userData.handle;
if (handle) handle.rotation.x = b.lit ? -Math.PI / 4 : 0;
b.mesh.traverse(child => {
if (child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material];
for (const m of mats) {
if (b.lit) { m.emissive = new THREE.Color(0xe83229); m.emissiveIntensity = 0.3; }
else { m.emissive = new THREE.Color(0x000000); m.emissiveIntensity = 0; }
}
}
});
simulate();
showToast(b.lit ? '拉杆: 开' : '拉杆: 关');
return;
}
// 右键交互: 按钮按下
if (b && b.type === 'button') {
if (b.lit) return;
b.lit = true;
const bmats = Array.isArray(b.mesh.material) ? b.mesh.material : [b.mesh.material];
for (const m of bmats) {
if (m.emissive) { m.emissive = new THREE.Color(0xe83229); m.emissiveIntensity = 0.5; }
}
b.mesh.scale.y = 0.5;
simulate();
showToast('按钮: 按下');
setTimeout(() => {
b.lit = false;
const rmats = Array.isArray(b.mesh.material) ? b.mesh.material : [b.mesh.material];
for (const m of rmats) {
if (m.emissive) { m.emissive = new THREE.Color(0x000000); m.emissiveIntensity = 0; }
}
b.mesh.scale.y = 1;
simulate();
}, 1000);
return;
}
// 否则放置方块
const nx = t.x + Math.round(t.normal.x);
const ny = t.y + Math.round(t.normal.y);
const nz = t.z + Math.round(t.normal.z);
const px = Math.floor(player.pos.x), py = Math.floor(player.pos.y), pz = Math.floor(player.pos.z);
if (nx === px && (ny === py || ny === py - 1) && nz === pz) { showToast('不能在脚下放置'); return; }
const bi = hotbarSlots[selectedSlot];
if (bi == null) { showToast('请选择方块'); return; }
const blockId = BLOCKS[bi].id;
// 方向性元件: 吸附到4个正方向 (前后左右)
const dir = isDirectional(blockId) ? Math.round((yaw + Math.PI) / (Math.PI / 2)) * (Math.PI / 2) : undefined;
placeBlock(nx, ny, nz, blockId, false, dir);
}
// ==================== 物理 ====================
// 确保玩家出生点不在方块内部(防止被卡住无法移动)
function ensurePlayerFree() {
const x = Math.floor(player.pos.x), z = Math.floor(player.pos.z);
// 从高处向下找第一个实心方块,玩家站到它上方 2 格
let top = -64;
for (let y = 200; y >= -64; y--) {
if (getBlock(x, y, z)) { top = y; break; }
}
const spawnY = (top === -64) ? 15 : top + 2;
player.pos.set(x + 0.5, spawnY, z + 0.5);
player.vel.set(0, 0, 0);
player.onGround = false;
}
function updatePlayer(dt) {
const fwd = new THREE.Vector3(-Math.sin(yaw), 0, -Math.cos(yaw));
const rgt = new THREE.Vector3(Math.cos(yaw), 0, -Math.sin(yaw));
const m = new THREE.Vector3();
if (keys['KeyW']) m.add(fwd);
if (keys['KeyS']) m.sub(fwd);
if (keys['KeyA']) m.sub(rgt);
if (keys['KeyD']) m.add(rgt);
if (m.lengthSq() > 0) m.normalize().multiplyScalar(SPEED);
player.vel.x = m.x; player.vel.z = m.z;
player.vel.y -= GRAVITY * dt;
if (player.vel.y < -MAX_FALL) player.vel.y = -MAX_FALL; // 限制终端速度,防止高速穿透方块
if (keys['Space'] && player.onGround) { player.vel.y = JUMP; player.onGround = false; }
const h = P_W / 2;
player.pos.x += player.vel.x * dt; if (collide(player.pos, h)) { player.pos.x -= player.vel.x * dt; player.vel.x = 0; }
player.pos.z += player.vel.z * dt; if (collide(player.pos, h)) { player.pos.z -= player.vel.z * dt; player.vel.z = 0; }
// 垂直移动分步(≤0.5格/步),保证任何帧率下都不会穿透方块
let dy = player.vel.y * dt;
while (dy !== 0) {
const s = Math.sign(dy) * Math.min(Math.abs(dy), 0.5);
player.pos.y += s;
if (collide(player.pos, h)) {
if (s < 0) {
// 落地:精确贴到方块顶面,消除穿透与贴地抖动
player.onGround = true;
player.pos.y = Math.floor(player.pos.y) + 1;
} else {
// 撞头:回退本步位移
player.pos.y -= s;
}
player.vel.y = 0;
dy = 0;
} else dy -= s;
}
if (player.pos.y < -20) { player.pos.set(0, 15, 0); player.vel.set(0,0,0); }
}
function collide(p, h) {
for (let x = Math.floor(p.x-h); x <= Math.floor(p.x+h); x++)
for (let y = Math.floor(p.y); y <= Math.floor(p.y+P_H); y++)
for (let z = Math.floor(p.z-h); z <= Math.floor(p.z+h); z++) {
const b = getBlock(x,y,z);
if (!b) continue;
const def = getDef(b.type);
// 小方块不参与碰撞
if (def.isButton || def.isLever || def.isTorch || def.isDust || def.isRepeater || def.isDiode || def.isNPN || def.isGate) continue;
return true;
}
return false;
}
// ==================== 渲染 ====================
function updateCamera() {
camera.position.set(player.pos.x, player.pos.y + P_H * 0.9, player.pos.z);
const dir = new THREE.Vector3(-Math.sin(yaw)*Math.cos(pitch), Math.sin(pitch), -Math.cos(yaw)*Math.cos(pitch));
camera.lookAt(camera.position.clone().add(dir));
}
function updateHighlight() {
const t = getTarget();
if (t) {
const b = getBlock(t.x, t.y, t.z);
const yOff = (b && b.mesh && b.mesh.userData) ? (b.mesh.userData.yOffset || 0) : 0;
highlight.position.set(t.x+0.5, t.y+0.5+yOff, t.z+0.5);
highlight.visible = true;
// 破坏进度显示:方块变暗
if (breakingBlock && breakingBlock.x === t.x && breakingBlock.y === t.y && breakingBlock.z === t.z) {
const p = breakingBlock.progress;
highlight.material.color.setRGB(0.2, 0.2, 0.2);
highlight.material.opacity = 0.3 + p * 0.5;
} else {
highlight.material.color.setRGB(0, 0, 0);
highlight.material.opacity = 1;
}
if (b) {
if (updateHighlight._lastName !== b.type) {
document.getElementById('looking').textContent = getDef(b.type).name;
updateHighlight._lastName = b.type;
}
}
} else {
highlight.visible = false;
if (updateHighlight._lastName !== '-') {
document.getElementById('looking').textContent = '-';
updateHighlight._lastName = '-';
}
}
}
function showToast(text) {
const t = document.getElementById('toast');
t.textContent = text; t.classList.add('show');
clearTimeout(showToast._t); showToast._t = setTimeout(() => t.classList.remove('show'), 1200);
}
function updateLoadProgress(pct, msg) {
document.getElementById('loadFill').style.width = pct + '%';
if (msg) {
document.getElementById('loadMsg').textContent = msg;
document.getElementById('loadSub').textContent = pct + '%';
}
if (pct >= 100) setTimeout(() => { document.getElementById('loading').style.display = 'none'; }, 300);
}
function debugHUD(msg) {
// 仅输出到控制台,不再写 DOM(避免高频日志导致卡顿)
if (window.DEBUG_HUD) {
var hud = document.getElementById('debugHUD');
if (hud) {
hud.style.display = 'block';
hud.innerHTML += '<div>[' + new Date().toLocaleTimeString() + '] ' + msg + '</div>';
if (hud.children.length > 20) hud.removeChild(hud.firstChild);
}
}
if (window.DEBUG_CONSOLE) console.log('[RC] ' + msg);
}
// ==================== 主循环 ====================
function gameLoop(now) {
const dt = Math.min((now - lastT) / 1000, 0.05);
lastT = now;
frames++; fpsT += dt;
if (fpsT >= 0.5) { fps = Math.round(frames / fpsT); frames = 0; fpsT = 0; }
// 模式目标检查
checkModeObjective();
// 限时模式倒计时
if (currentGameMode === 'multiplayer' && !puzzleObjective?.completed) {
const remaining = Math.max(0, timeAttackDuration - Math.floor((now - timeAttackStart) / 1000));
const m = Math.floor(remaining/60), s = remaining%60;
document.getElementById('modeHUD-timer').textContent = m+':'+String(s).padStart(2,'0');
document.getElementById('modeHUD-score').textContent = '⚡ ' + remaining + 's';
if (remaining <= 0 && !puzzleObjective.completed) {
puzzleObjective.completed = true;
document.getElementById('modeHUD-timer').textContent = '0:00';
showToast('时间到!');
}
}
// 破坏进度更新
if (breakingBlock && gameActive) {
const b = getBlock(breakingBlock.x, breakingBlock.y, breakingBlock.z);
if (!b) { breakingBlock = null; }
else {
const elapsed = now - breakStartTime;
breakingBlock.progress = Math.min(elapsed / BREAK_TIME, 1);
if (elapsed >= BREAK_TIME) finishBreak();
}
}
if (gameActive) updatePlayer(dt);
updateCamera(); updateHighlight();
// HUD 信息节流更新:每 10 帧写一次 DOM,避免每帧布局抖动
if (frames % 10 === 0) {
document.getElementById('pos').textContent = `${player.pos.x.toFixed(1)}, ${player.pos.y.toFixed(1)}, ${player.pos.z.toFixed(1)}`;
document.getElementById('bcount').textContent = world.size;
document.getElementById('fps').textContent = fps;
}
// 每 30 秒自动存档 (无限地形下 world 会增长,降低频率避免序列化卡顿)
if (Math.floor(now / 30000) !== Math.floor(lastSaveTime / 30000)) { saveWorld(); lastSaveTime = now; }
// 无限地形:仅在玩家跨区块时加载新区块 (避免每帧全量扫描)
if (window._worldLoaded && frames % 5 === 0) {
var ccx = Math.floor(player.pos.x / CHUNK_SIZE);
var ccz = Math.floor(player.pos.z / CHUNK_SIZE);
if (ccx !== lastPlayerChunkX || ccz !== lastPlayerChunkZ) {
lastPlayerChunkX = ccx; lastPlayerChunkZ = ccz;
ensureTerrainAround(player.pos.x, player.pos.z);
}
}
renderer.render(scene, camera);
requestAnimationFrame(gameLoop);
}
let lastSaveTime = 0;
let lastPlayerChunkX = 0, lastPlayerChunkZ = 0;
// ==================== 启动 ====================
// 背景图片绘制
function drawMCBackground() {
var c = document.getElementById('bgCanvas');
if (!c) return;
c.width = window.innerWidth; c.height = window.innerHeight;
var ctx = c.getContext('2d');
var img = new Image();
img.onload = function() {
// 按 cover 模式绘制:等比缩放并居中裁剪
var iw = img.width, ih = img.height;
var cw = c.width, ch = c.height;
var scale = Math.max(cw / iw, ch / ih);
var sw = iw * scale, sh = ih * scale;
var sx = (cw - sw) / 2, sy = (ch - sh) / 2;
ctx.drawImage(img, sx, sy, sw, sh);
};
img.src = 'background.jpg';
}
function init() {
debugHUD('init() 开始');
updateLoadProgress(2, '引擎初始化中...');
// WebGL 诊断:如果之前检测到 WebGL 不可用,显示错误页
if (window._webglFailed) {
updateLoadProgress(100, 'WebGL 不可用');
document.getElementById('loading').innerHTML =
'<div style="color:#F85149;text-align:center;max-width:400px">'+
'<h2>WebGL 不可用</h2>'+
'<p>你的显卡或 WebView2 环境不支持 WebGL。</p>'+
'<p style="font-size:12px;color:#888">'+(window._diagReport||[]).join('<br>')+'</p>'+
'<button onclick="this.parentElement.style.display=\'none\'" style="margin-top:12px;padding:8px 20px;background:#E83229;color:#fff;border:none;border-radius:2px;cursor:pointer">关闭</button>'+
'</div>';
return;
}
// 创建场景(初始透明,让背景图透过)
scene = new THREE.Scene();
scene.background = null;
camera = new THREE.PerspectiveCamera(65, innerWidth/innerHeight, 0.1, 100);
updateCamera();
debugHUD('Scene created (transparent bg)');
updateLoadProgress(5, '创建渲染器...');
// 创建渲染器(多级降级)
renderer = null;
var renderOpts = [
{ antialias: false, alpha: true, premultipliedAlpha: false, powerPreference: 'default', failIfMajorPerformanceCaveat: false },
{ antialias: false, alpha: true, powerPreference: 'low-power' },
{ antialias: false, alpha: true },
{ alpha: true }
];
for (var i = 0; i < renderOpts.length; i++) {
try {
renderer = new THREE.WebGLRenderer(renderOpts[i]);
debugHUD('WebGLRenderer OK (opts#'+i+')');
break;
} catch(e) {
debugHUD('Renderer FAIL #'+i+': '+e.message);
}
}
if (!renderer) {
updateLoadProgress(100, '渲染器初始化失败');
debugHUD('FATAL: No WebGLRenderer');
document.getElementById('loading').innerHTML =
'<div style="color:#F85149;text-align:center">'+
'<h2>3D渲染器初始化失败</h2>'+
'<p>你的 WebView2 环境不支持 WebGL。</p>'+
'<p style="font-size:12px;color:#888">请更新显卡驱动或安装 WebView2 Runtime</p>'+
'<button onclick="this.parentElement.style.display=\'none\'" style="margin-top:12px;padding:8px 20px;background:#E83229;color:#fff;border:none;border-radius:2px;cursor:pointer">关闭</button>'+
'</div>';
return;
}
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.0));
renderer.domElement.style.position = 'fixed';
renderer.domElement.style.inset = '0';
renderer.domElement.style.zIndex = '1';
document.body.appendChild(renderer.domElement);
debugHUD('Canvas appended');
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const dl = new THREE.DirectionalLight(0xffffff, 0.5);
dl.position.set(0.5, 1, 0.3); scene.add(dl);
scene.add(new THREE.HemisphereLight(0xaaccff, 0x445533, 0.3));
raycaster = new THREE.Raycaster();
const hlMat = new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true, transparent: true, opacity: 0.5, depthTest: false });
const hl = new THREE.Mesh(new THREE.BoxGeometry(1.02, 1.02, 1.02), hlMat);
hl.visible = false; scene.add(hl); highlight = hl;
// 首次渲染
try { renderer.render(scene, camera); debugHUD('First render OK'); }
catch(e) { debugHUD('First render FAIL: '+e.message); }
// 设置事件
setupEvents();
// 启动渲染循环(没有世界的状态下正常渲染天空背景)
requestAnimationFrame(gameLoop);
updateLoadProgress(100, '就绪 · 点击开始游戏');
debugHUD('=== READY (waiting for start) ===');
}
// 开始游戏时调用:生成地形、构建世界
function startWorld() {
if (window._worldLoaded) return;
window._worldLoaded = true;
debugHUD('startWorld() called');
document.getElementById('loading').style.display = 'flex';
updateLoadProgress(10, '正在生成世界...');
setTimeout(() => {
try {
updateLoadProgress(25, '初始化物品数据...');
debugHUD('initInventoryData...');
initInventoryData();
debugHUD('inventory done');
updateLoadProgress(30, '生成地形...');
if (!loadWorld()) {
debugHUD('generateInitialTerrain...');
ensureTerrainAround(0, 0);
debugHUD('initial terrain done');
} else {
debugHUD('world loaded from save');
}
// 设置天空背景(扩大雾距适应高层世界)
scene.background = new THREE.Color(0x88aacc);
scene.fog = new THREE.Fog(0x88aacc, 64, 256);
debugHUD('sky background set');
updateLoadProgress(80, '构建快捷栏...');
createHotbar();
debugHUD('hotbar done');
updateLoadProgress(100, '就绪!');
debugHUD('=== WORLD READY ===');
window._worldLoaded = true;
// 隐藏加载屏,进入游戏
setTimeout(function(){
document.getElementById('loading').style.display = 'none';
document.getElementById('overlay').style.display = 'none';
document.getElementById('menuParticles').innerHTML = '';
document.getElementById('modeHUD').style.display = 'flex';
updateModeHUD();
// 确保出生点不在方块内部,否则会被卡住无法移动
ensurePlayerFree();
gameActive = true;
try {
const p = renderer.domElement.requestPointerLock();
// 若锁定失败,稍后检测并提示拖动视角
setTimeout(() => {
if (gameActive && document.pointerLockElement !== renderer.domElement) {
showToast('按住鼠标拖动旋转视角 · WASD 移动');
}
}, 400);
} catch(e) {
showToast('按住鼠标拖动旋转视角 · WASD 移动');
}
}, 500);
} catch(e) {
debugHUD('FATAL: '+e.message);
updateLoadProgress(100, '错误: '+e.message);
document.getElementById('loading').innerHTML = '世界生成失败: ' + e.message + '<br><button onclick="this.parentElement.style.display=\'none\'" style="margin-top:12px;padding:8px 20px;background:#E83229;color:#fff;border:none;border-radius:2px;cursor:pointer">继续</button>';
}
}, 50);
// 超时强制显示
setTimeout(function(){
var ld = document.getElementById('loading');
if (ld && ld.style.display !== 'none') {
debugHUD('TIMEOUT: force hide loading');
ld.style.display = 'none';
}
}, 30000);
}
drawMCBackground();
init();
// SSO 自动登录:从 URL 参数读取启动器传来的 token
(function(){
var p = new URLSearchParams(location.search);
var token = p.get('token');
var username = p.get('username');
if (token && username && p.get('autoLogin') === '1') {
// 先设置认证状态,然后用 /me 验证 token
authState.token = token;
authState.user = { username: username };
authState.loggedIn = true;
authState.offline = false;
// 异步验证 token 有效性
fetch(AUTH_URL + '/me', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r) { return r.json(); })
.then(function(d) {
if (d.code === 0 && d.data) {
authState.user = d.data;
authState.loggedIn = true;
showPanel('hub');
}
}).catch(function() {
// token 验证失败,保持基本状态(用户名至少能用离线模式)
showPanel('hub');
});
// 先直接跳到 hub,不等验证结果
setTimeout(function() { showPanel('hub'); }, 500);
}
})();
</script>
</body>
</html>