feat: 白色主题UI + 签到接口修复 + 物理/地形Bug修复 + WPF启动器重构
CI / Go Backend (push) Canceled after 0s

This commit is contained in:
xyou
2026-08-09 21:32:55 +08:00
parent f70b061d1a
commit abd4548dff
20 changed files with 3559 additions and 449 deletions
+2528 -252
View File
@@ -2,69 +2,682 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>RedCircuit · 方块世界 v0.3</title>
<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}
body{overflow:hidden;background:#000;font-family:'Segoe UI',sans-serif}
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.7)}
#crosshair::before{width:2px;height:12px;left:9px;top:4px}
#crosshair::after{width:12px;height:2px;left:4px;top:9px}
#hotbar{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:4px;background:rgba(0,0,0,0.5);padding:4px;border-radius:6px;z-index:10}
.slot{width:56px;height:64px;border:2px solid rgba(255,255,255,0.2);border-radius:4px;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;cursor:pointer;position:relative;background:rgba(0,0,0,0.3);padding-top:2px}
.slot.active{border-color:#E83229;box-shadow:0 0 8px rgba(232,50,41,0.5)}
.slot-icon{width:32px;height:32px;border-radius:3px;image-rendering:pixelated}
.slot-key{position:absolute;top:1px;left:3px;font-size:9px;color:rgba(255,255,255,0.6)}
.slot-name{font-size:9px;color:rgba(255,255,255,0.7);margin-top:2px;text-align:center;line-height:1.1;white-space:nowrap}
#info{position:fixed;top:12px;left:12px;color:#aaa;font-size:12px;z-index:10;line-height:1.6;background:rgba(0,0,0,0.4);padding:8px 12px;border-radius:6px}
#info b{color:#E83229}
#overlay{position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;flex-direction:column;z-index:100;color:#fff}
#overlay h1{font-size:32px;color:#E83229;margin-bottom:8px}
#overlay p{color:#888;margin:4px 0;font-size:14px}
#overlay .start-btn{margin-top:20px;padding:12px 40px;background:#E83229;border:none;color:#fff;font-size:16px;border-radius:6px;cursor:pointer}
#overlay .start-btn:hover{background:#B91C1C}
#overlay .keys{margin-top:16px;font-size:12px;color:#666;line-height:1.8;text-align:center}
#toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-60px);background:rgba(0,0,0,0.7);color:#fff;padding:6px 16px;border-radius:4px;font-size:13px;z-index:20;opacity:0;transition:opacity .3s;pointer-events:none}
#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}
#inventory{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:none;align-items:center;justify-content:center;z-index:50}
/* ---------- 游戏内背包(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:#C6C6C6;border:4px solid #555;border-radius:0;padding:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;gap:8px}
.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:12px;color:#333;font-weight:bold;margin-bottom:4px;text-align:center}
.inv-storage{display:grid;grid-template-columns:repeat(9,36px);gap:1px;justify-content:center;margin-bottom:4px}
.inv-hotbar{display:grid;grid-template-columns:repeat(10,36px);gap:1px;justify-content:center;border-top:2px solid #555;padding-top:4px;margin-top:2px}
.inv-creative{display:grid;grid-template-columns:repeat(6,36px);gap:1px;justify-content:center;width:222px;max-height:288px;overflow-y:auto;padding:2px;background:#8B8B8B;border:2px solid;border-color:#373737 #fff #fff #373737}
.inv-creative::-webkit-scrollbar{width:8px}
.inv-creative::-webkit-scrollbar-track{background:#8B8B8B}
.inv-creative::-webkit-scrollbar-thumb{background:#555}
.inv-slot{width:36px;height:36px;background:#8B8B8B;border:2px solid;border-color:#373737 #fff #fff #373737;display:flex;align-items:center;justify-content:center;cursor:pointer;position:relative}
.inv-slot:hover{background:#9B9B9B}
.inv-slot.active{outline:2px solid #fff;outline-offset:-1px}
.inv-slot canvas,.inv-slot img{width:28px;height:28px;image-rendering:pixelated;pointer-events:none}
.inv-slot .slot-num{position:absolute;top:0px;left:1px;font-size:7px;color:rgba(255,255,255,0.5);pointer-events:none}
.inv-cat-label{font-size:9px;color:#404040;margin:2px 0 1px;padding-left:2px;text-align:left;width:100%}
.inv-sep{height:1px;background:#666;margin:2px 0}
.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:28px;height:28px;image-rendering:pixelated}
.inv-hint{text-align:center;font-size:9px;color:#555;margin-top:3px}
#loading{position:fixed;inset:0;background:#0E1116;display:flex;align-items:center;justify-content:center;flex-direction:column;z-index:200;color:#888}
#loading .bar{width:200px;height:4px;background:#2A3441;border-radius:2px;margin-top:12px;overflow:hidden}
#loading .fill{height:100%;background:#E83229;width:0;transition:width .3s}
#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>
<div id="loading"><div>正在生成世界...</div><div class="bar"><div class="fill" id="loadFill"></div></div></div>
<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">
@@ -80,14 +693,184 @@ canvas{display:block}
</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">
<h1>RedCircuit</h1>
<p>红石回路 · 方块世界原型</p>
<button class="start-btn" id="startBtn">点击进入世界</button>
<div class="keys">WASD 移动 · 空格跳跃 · 鼠标视角<br>左键破坏 · 右键放置 · 数字 1-9 快捷栏<br><b>E 打开背包</b> · ESC 暂停</div>
<!-- 欢迎面板 -->
<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) {
@@ -368,40 +1151,1084 @@ const HOTBAR_SIZE = 10; // 快捷栏显示前10个 (1-9 + 0)
// ==================== 方向性元件引脚定义 ====================
// 默认朝向: 输出=+Z (前方), 输入=-Z (后方)
// pin: [x, z, length, color] x/z=中心偏移, length=杆长, color=颜色
// 引脚从元件体边缘延伸到相邻方块内部 (±0.5 为格子边界)
// pin: [x, z, length, color] 引脚从元件体(±0.2)延伸到相邻方块表面(±0.5)
const PIN_LAYOUTS = {
diode: { pins: [[0,-0.5,0.6,0x6080a0],[0,0.5,0.6,0xe83229]] },
npn: { pins: [[0,-0.5,0.6,0x6080a0],[0.5,0,0.6,0xe83229],[-0.5,0,0.6,0x3FB950]] },
andGate: { pins: [[-0.2,-0.5,0.6,0x6080a0],[0.2,-0.5,0.6,0x6080a0],[0,0.5,0.6,0xe83229]] },
orGate: { pins: [[-0.2,-0.5,0.6,0x6080a0],[0.2,-0.5,0.6,0x6080a0],[0,0.5,0.6,0xe83229]] },
notGate: { pins: [[0,-0.5,0.6,0x6080a0],[0,0.5,0.6,0xe83229]] },
repeater: { pins: [[0,-0.5,0.6,0x6080a0],[0,0.5,0.6,0xe83229]] },
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 = 20; // 世界半径 40x40
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) {
@@ -466,25 +2293,36 @@ function makeBlockMesh(blockDef) {
return mesh;
}
function getDef(id) { return BLOCKS.find(b => b.id === id); }
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);
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
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();
}
mesh.userData = { x, y, z, type: typeId, yOffset, direction: dir };
scene.add(mesh);
world.set(k, { type: typeId, mesh, lit: false });
if (!skipUpdate) updateLamps();
return true;
}
@@ -492,139 +2330,182 @@ function removeBlock(x, y, z) {
const k = bk(x, y, z);
const b = world.get(k);
if (!b) return false;
scene.remove(b.mesh);
// 释放资源 (支持 Group)
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();
}
});
// 释放独立 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);
updateLamps();
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 updateLamps() {
// ==================== 电路仿真引擎 ====================
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 = 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) continue;
const nd = getDef(n.type);
if (nd.power) { powered = true; break; }
if (n.type === 'lever' && n.lit) { powered = true; break; }
if (n.type === 'button' && n.lit) { powered = true; break; }
if (n.type === 'torch') { powered = true; break; } // 火把始终供能
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;
const newFaces = powered ? def.litFaces : def.faces;
b.mesh.material.forEach((m, i) => { m.map = newFaces[i]; m.needsUpdate = true; });
if (powered) b.mesh.material.forEach(m => { m.emissive = new THREE.Color(0xfcd34d); m.emissiveIntensity = 0.3; });
else b.mesh.material.forEach(m => { m.emissive = new THREE.Color(0x000000); m.emissiveIntensity = 0; });
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;
});
}
}
}
// ==================== 地形生成 ====================
function generateTerrain() {
const noise = new PerlinNoise(42);
const treeNoise = new PerlinNoise(99);
const heightMap = {};
// 生成高度图
for (let x = -WORLD_R; x < WORLD_R; x++) {
for (let z = -WORLD_R; z < WORLD_R; z++) {
// 多层噪声: 大地形 + 细节
const base = noise.octave(x/24, z/24, 4, 0.5); // -1~1
const detail = noise.octave(x/8, z/8, 2, 0.3) * 0.3;
const h = Math.floor(base * 5 + detail * 3 + 3); // 高度范围约 -2 ~ 11
heightMap[bk(x,z)] = h;
// 填充方块
for (let y = 0; y <= h; y++) {
let type;
if (y === h) {
if (h <= SEA_LEVEL) type = 'sand';
else type = 'grass';
} else if (y >= h - 2) {
type = h <= SEA_LEVEL + 1 ? 'sand' : 'dirt';
} else {
type = 'stone';
}
placeBlock(x, y, z, type, true);
}
// 水面
if (h < SEA_LEVEL) {
for (let y = h + 1; y <= SEA_LEVEL; y++) {
placeBlock(x, y, z, 'sand', true); // 浅水区用沙子
}
}
}
}
// 生成树木
let treeCount = 0;
for (let x = -WORLD_R + 2; x < WORLD_R - 2; x++) {
for (let z = -WORLD_R + 2; z < WORLD_R - 2; z++) {
const h = heightMap[bk(x, z)];
if (h < SEA_LEVEL + 1) continue; // 水中不生树
const surface = getBlock(x, h, z);
if (!surface || surface.type !== 'grass') continue;
// 树木概率: 基于噪声 + 随机
const tn = treeNoise.noise(x * 0.5, z * 0.5);
if (tn > 0.4 && Math.random() < 0.08 && treeCount < 30) {
generateTree(x, h + 1, z);
treeCount++;
}
}
}
// 示例红石电路
const sx = 3, sz = 3;
const sh = heightMap[bk(sx, sz)] || 3;
placeBlock(sx, sh + 1, sz, 'rblock', true);
placeBlock(sx + 1, sh + 1, sz, 'lamp', true);
const lx = -3, lz = -3;
const lh = heightMap[bk(lx, lz)] || 3;
placeBlock(lx, lh + 1, lz, 'lever', true);
placeBlock(lx + 1, lh + 1, lz, 'lamp', true);
updateLamps();
updateLoadProgress(100);
}
// generateTerrain 已替换为 generateTerrainColumn + ensureTerrainAround 无限地形系统
function generateTree(x, y, z) {
const trunkH = 4 + (Math.random() * 2 | 0);
// 树干
for (let i = 0; i < trunkH; i++) placeBlock(x, y + i, z, 'wood', true);
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;
placeBlock(x + dx, top - 1, z + dz, 'leaves', true);
setBlockData(x + dx, top - 1, z + dz, 'leaves');
}
// 中层 3x3
for (let dx = -1; dx <= 1; dx++)
for (let dz = -1; dz <= 1; dz++)
placeBlock(x + dx, top, z + dz, 'leaves', true);
setBlockData(x + dx, top, z + dz, 'leaves');
// 顶层
placeBlock(x, top + 1, z, 'leaves', true);
if (Math.random() < 0.5) placeBlock(x + 1, top + 1, z, 'leaves', true);
if (Math.random() < 0.5) placeBlock(x, top + 1, z + 1, 'leaves', true);
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 风格) ====================
@@ -686,7 +2567,7 @@ function createInventory() {
// 右侧: 存储区 (18格) + 快捷栏 (10格)
const storage = document.getElementById('invStorage');
storage.innerHTML = '';
for (let i = 0; i < 18; i++) storage.appendChild(makeInvSlot(invSlots[i], i, 'storage'));
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++) {
@@ -703,6 +2584,10 @@ function makeInvSlot(blockIdx, slotIdx, 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');
@@ -741,11 +2626,20 @@ function updateCarriedItem() {
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 {
@@ -756,6 +2650,7 @@ function toggleInventory(force) {
else hotbarSlots[hotbarSlots.indexOf(null)] = carriedItem;
carriedItem = null; updateCarriedItem();
}
gameActive = true;
renderer.domElement.requestPointerLock();
}
}
@@ -771,91 +2666,197 @@ function selectSlot(i) {
// 初始化背包数据
function initInventoryData() {
hotbarSlots = BLOCKS.map((_, i) => i).slice(0, HOTBAR_SIZE);
invSlots = new Array(18).fill(null);
invSlots = new Array(27).fill(null);
const remaining = BLOCKS.map((_, i) => i).slice(HOTBAR_SIZE);
for (let i = 0; i < remaining.length && i < 18; i++) invSlots[i] = remaining[i];
for (let i = 0; i < remaining.length && i < 27; i++) invSlots[i] = remaining[i];
}
// ==================== 事件 ====================
function setupEvents() {
document.getElementById('startBtn').addEventListener('click', () => {
document.getElementById('overlay').style.display = 'none';
renderer.domElement.requestPointerLock();
// 暂停面板按钮
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; // 背包打开时不显示暂停
const ov = document.getElementById('overlay');
if (ov.style.display === 'none') {
ov.querySelector('h1').textContent = '已暂停';
ov.querySelector('.start-btn').textContent = '点击继续';
ov.style.display = 'flex';
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 (document.pointerLockElement !== renderer.domElement) return;
yaw -= e.movementX * 0.0025;
pitch -= e.movementY * 0.0025;
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; }
if (e.code === 'Escape' && document.getElementById('inventory').classList.contains('show')) { toggleInventory(false); 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 (document.pointerLockElement !== renderer.domElement) return;
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 - 20) + 'px';
el.style.top = (e.clientY - 20) + 'px';
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() {
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
raycaster.far = REACH;
const meshes = [];
for (const [, b] of world) if (!getDef(b.type).transparent || b.type !== 'leaves') meshes.push(b.mesh);
else meshes.push(b.mesh);
const hits = raycaster.intersectObjects(meshes, true);
if (!hits.length) return null;
const h = hits[0];
// 向上查找有方块坐标的父对象 (支持 Group)
let obj = h.object;
while (obj && obj.userData.x === undefined) obj = obj.parent;
if (!obj) return null;
return { x: obj.userData.x, y: obj.userData.y, z: obj.userData.z, normal: h.face.normal };
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) return;
if (b.type === 'lever') {
// 右键交互: 拉杆切换开关
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];
@@ -865,19 +2866,20 @@ function breakBlock() {
}
}
});
updateLamps();
simulate();
showToast(b.lit ? '拉杆: 开' : '拉杆: 关');
return;
}
if (b.type === 'button') {
if (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;
updateLamps();
simulate();
showToast('按钮: 按下');
setTimeout(() => {
b.lit = false;
@@ -886,16 +2888,11 @@ function breakBlock() {
if (m.emissive) { m.emissive = new THREE.Color(0x000000); m.emissiveIntensity = 0; }
}
b.mesh.scale.y = 1;
updateLamps();
simulate();
}, 1000);
return;
}
removeBlock(t.x, t.y, t.z);
}
function placeBlockAction() {
const t = getTarget();
if (!t) 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);
@@ -904,12 +2901,26 @@ function placeBlockAction() {
const bi = hotbarSlots[selectedSlot];
if (bi == null) { showToast('请选择方块'); return; }
const blockId = BLOCKS[bi].id;
// 方向性元件: 输出方向 = 玩家视角方向 (yaw + π 使输出朝向玩家面朝方向)
const dir = isDirectional(blockId) ? yaw + Math.PI : undefined;
// 方向性元件: 吸附到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));
@@ -921,16 +2932,30 @@ function updatePlayer(dt) {
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; }
player.pos.y += player.vel.y * dt;
if (collide(player.pos, h)) {
if (player.vel.y < 0) player.onGround = true;
player.pos.y -= player.vel.y * dt; player.vel.y = 0;
} else player.onGround = false;
// 垂直移动分步(≤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); }
}
@@ -960,13 +2985,30 @@ function updateHighlight() {
const t = getTarget();
if (t) {
const b = getBlock(t.x, t.y, t.z);
const yOff = (b && b.mesh.userData.yOffset) || 0;
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 (b) document.getElementById('looking').textContent = getDef(b.type).name;
// 破坏进度显示:方块变暗
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;
document.getElementById('looking').textContent = '-';
if (updateHighlight._lastName !== '-') {
document.getElementById('looking').textContent = '-';
updateHighlight._lastName = '-';
}
}
}
@@ -976,10 +3018,26 @@ function showToast(text) {
clearTimeout(showToast._t); showToast._t = setTimeout(() => t.classList.remove('show'), 1200);
}
function updateLoadProgress(pct) {
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) {
@@ -987,26 +3045,137 @@ function gameLoop(now) {
lastT = now;
frames++; fpsT += dt;
if (fpsT >= 0.5) { fps = Math.round(frames / fpsT); frames = 0; fpsT = 0; }
if (document.pointerLockElement === renderer.domElement) updatePlayer(dt);
// 模式目标检查
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();
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;
// 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 init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x88aacc);
scene.fog = new THREE.Fog(0x88aacc, 25, 70);
// 背景图片绘制
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';
}
camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 200);
renderer = new THREE.WebGLRenderer({ antialias: false });
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.5));
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);
@@ -1014,25 +3183,132 @@ function init() {
scene.add(new THREE.HemisphereLight(0xaaccff, 0x445533, 0.3));
raycaster = new THREE.Raycaster();
const hl = new THREE.Mesh(
new THREE.BoxGeometry(1.02, 1.02, 1.02),
new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true })
);
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;
// 异步生成世界 (避免阻塞)
updateLoadProgress(10);
setTimeout(() => {
updateLoadProgress(30);
initInventoryData();
generateTerrain();
createHotbar();
setupEvents();
requestAnimationFrame(gameLoop);
}, 50);
// 首次渲染
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>