game/index.html
2026-07-28 19:06:32 +08:00

972 lines
59 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>宇森 · Yusen活的文明系统模拟</title>
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" /></noscript>
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="icon" type="image/png" sizes="256x256" href="assets/ui/logo_planet_256.png?v=20260723">
<link rel="apple-touch-icon" href="assets/ui/logo_planet_256.png?v=20260723">
<style>
:root{
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
--text:#e9edf6; --text-dim:#9aa6c4;
--card:rgba(255,255,255,.045); --card-bd:rgba(255,255,255,.10);
--glow:0 0 22px rgba(139,92,246,.28);
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;width:100%;overflow:hidden;}
body{
font-family:"Noto Sans SC",system-ui,sans-serif;color:var(--text);
background:
radial-gradient(1000px 620px at 82% -8%, rgba(139,92,246,.14), transparent 60%),
radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.10), transparent 55%),
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 48%, var(--bg-2) 100%);
position:relative;
}
body::before,body::after{content:"";position:fixed;border-radius:50%;filter:blur(80px);z-index:0;pointer-events:none;}
body::before{width:380px;height:380px;background:rgba(139,92,246,.20);top:-110px;right:-70px;animation:fl1 20s ease-in-out infinite;}
body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:fl2 24s ease-in-out infinite;}
@keyframes fl1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
@keyframes fl2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
.app{position:relative;z-index:2;height:100vh;display:flex;flex-direction:column;}
.topbar{height:64px;flex:0 0 auto;display:flex;align-items:center;gap:22px;padding:0 30px;border-bottom:1px solid var(--card-bd);background:rgba(8,12,26,.55);backdrop-filter:blur(10px);}
.logo{display:flex;align-items:baseline;gap:9px;cursor:pointer;}
.logo .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;letter-spacing:4px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.55);}
.logo .en{font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;}
.nav{display:flex;gap:6px;margin-left:12px;}
.nav button{font-family:"Noto Sans SC",sans-serif;font-size:15px;font-weight:500;color:var(--text-dim);background:transparent;border:1px solid transparent;padding:8px 15px;border-radius:10px;cursor:pointer;transition:.22s;}
.nav button:hover{color:var(--text);background:rgba(139,92,246,.12);}
.nav button.active{color:#fff;background:rgba(139,92,246,.22);border-color:var(--card-bd);box-shadow:var(--glow);}
.spacer{flex:1;}
.user-box{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--text);}
.user-box .ub-name{color:#fff;font-weight:500;}
.user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
.user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
.user-box .auth-link{color:var(--text-dim);text-decoration:none;font-size:14px;transition:.2s;padding:5px 8px;}
.user-box .auth-link:hover{color:#fff;}
.user-box .auth-btn{font-family:inherit;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);border:none;padding:6px 16px;border-radius:20px;cursor:pointer;font-weight:600;transition:.2s;}
.user-box .auth-btn:hover{filter:brightness(1.1);box-shadow:0 0 16px rgba(231,184,92,.4);}
.nav .auth-only{display:none;} /* 登录专属 tab未登录时隐藏 */
.nav .auth-only.visible{display:inline-block;}
.cta:hover{filter:brightness(1.08);}
.content{flex:1;position:relative;overflow:hidden;}
.panel{position:absolute;inset:0;display:none;flex-direction:column;padding:28px 48px;overflow:hidden;}
.panel.active{display:flex;}
.panel-scroll{flex:1;min-height:0;overflow-y:auto;padding-right:8px;}
/* 隐藏内部滚动条但保留滚轮/触控滑动(规则/世界数据/公告/论坛/图鉴/剧情等) */
.rules-scroll,#worldDataContent,#announce .notes,.threads,.codex-grid,.story-reader,.panel-scroll,.toc-list{
scrollbar-width:none; -ms-overflow-style:none;
}
.rules-scroll::-webkit-scrollbar,#worldDataContent::-webkit-scrollbar,#announce .notes::-webkit-scrollbar,
.threads::-webkit-scrollbar,.codex-grid::-webkit-scrollbar,.story-reader::-webkit-scrollbar,
.panel-scroll::-webkit-scrollbar,.toc-list::-webkit-scrollbar{display:none;width:0;height:0;}
/* ===== 介绍(图片轮播主页) ===== */
#intro{padding:0;}
.hero{position:relative;flex:1;border-radius:0;overflow:hidden;}
.hero .slideshow{position:absolute;inset:0;overflow:hidden;}
.hero .slide-track{position:absolute;inset:0;display:flex;flex-direction:column;transition:transform 1.25s cubic-bezier(0.4,0,0.2,1);will-change:transform;}
.hero .slide{flex:0 0 100%;width:100%;height:100%;background-size:cover;background-position:center;position:relative;}
.hero .slide::before{content:"";position:absolute;inset:0;
background:linear-gradient(90deg, rgba(8,12,26,.92) 0%, rgba(8,12,26,.70) 22%, rgba(8,12,26,.40) 40%, transparent 58%),
linear-gradient(180deg, rgba(8,12,26,.55) 0%, transparent 35%);}
.hero .copy{position:absolute;left:56px;top:56px;z-index:3;pointer-events:none;}
.hero .copy h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:56px;letter-spacing:10px;color:#fff;text-shadow:0 2px 24px rgba(0,0,0,.9),0 0 32px rgba(139,92,246,.7);line-height:1.2;}
.hero .copy .sub2{font-family:"Press Start 2P",monospace;font-size:11px;color:var(--gold);margin:6px 0 0;letter-spacing:2px;text-shadow:0 1px 8px rgba(0,0,0,.9);}
.hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);}
/* 轮播左下角特色文案 */
.slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;}
.slide.active .slide-label{opacity:1;transform:translateY(0);}
.slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);}
.slide-label .sl-cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:46px;letter-spacing:7px;color:#fff;text-shadow:0 2px 22px rgba(0,0,0,.95),0 0 30px rgba(139,92,246,.65);line-height:1.25;}
.slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);}
/* 轮播指示器(竖排右侧) */
.sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;}
.sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;}
.sl-dot:hover{background:rgba(255,255,255,.5);}
.sl-dot.active{background:var(--gold);border-color:var(--gold);box-shadow:0 0 12px rgba(231,184,92,.6);transform:scale(1.15);}
/* 轮播右上角竖排「起源历史」 */
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94);
text-shadow:0 1px 3px rgba(0,0,0,.92);max-height:28vh;overflow:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
opacity:0;transform:translateY(-12px);transition:opacity .9s ease .25s, transform .9s ease .25s;}
.slide.active .slide-origin{opacity:1;transform:translateY(0);}
.slide-origin .so-tag{color:var(--gold);font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:19px;letter-spacing:5px;margin-bottom:7px;text-shadow:0 1px 4px rgba(0,0,0,.92),0 0 10px rgba(231,184,92,.45);}
.hero .enter-wrap{position:absolute;left:0;right:0;bottom:28px;display:flex;justify-content:center;z-index:5;}
.hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;}
.hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);}
/* ===== 特色 ===== */
#features{align-items:center;justify-content:center;gap:22px;}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:min(1060px,100%);}
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:18px 20px;transition:.26s;min-height:132px;}
.card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.12);}
.card .ic{font-size:28px;margin-bottom:8px;filter:drop-shadow(0 0 6px rgba(199,125,255,.6));}
.card h3{font-size:17px;color:#fff;margin-bottom:7px;letter-spacing:1px;}
.card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
/* ===== 背景 ===== */
#background{flex-direction:column;align-items:stretch;gap:0;overflow:hidden;padding:28px 48px;}
.bg-page{max-width:1080px;width:100%;margin:0 auto;padding:24px 0;border-top:1px solid var(--card-bd);}
.bg-page:first-child{border-top:none;padding-top:0;}
.bg-page-head{text-align:center;margin-bottom:28px;}
.bg-page-title{font-family:"Noto Serif SC",serif;font-weight:800;font-size:40px;letter-spacing:3px;line-height:1.2;margin:0 0 10px;
background:linear-gradient(120deg,#ffffff 0%,#f0d49a 48%,#c79bff 100%);
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.bg-page-sub{font-size:13px;letter-spacing:7px;color:var(--text-dim);text-transform:uppercase;}
.bg-page-body{font-size:17px;line-height:2.05;color:#e2e8f6;max-width:840px;margin:0 auto 34px;}
.bg-page-body p{margin:0 0 18px;}
.bg-page-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(262px,1fr));gap:18px;}
.bg-card{display:flex;gap:15px;align-items:flex-start;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.05));
border:1px solid var(--card-bd);border-radius:16px;padding:18px 20px;transition:.26s;}
.bg-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.16);}
.bg-card-ic{font-size:27px;line-height:1.15;filter:drop-shadow(0 0 9px rgba(139,92,246,.55));flex:0 0 auto;}
.bg-card-title{font-size:17px;font-weight:700;color:#fff;margin-bottom:6px;}
.bg-card-desc{font-size:14.5px;line-height:1.66;color:var(--text-dim);}
.bg-empty{text-align:center;color:var(--text-dim);padding:70px 0;font-size:16px;}
.layers{flex:0 0 270px;display:flex;flex-direction:column;gap:8px;justify-content:center;}
.layer{border-radius:12px;padding:11px 15px;border:1px solid var(--card-bd);position:relative;}
.layer .lt{font-size:15px;color:#fff;font-weight:500;letter-spacing:1px;}
.layer .ld{font-size:12px;color:var(--text-dim);margin-top:4px;line-height:1.5;}
.layer.l1{background:linear-gradient(100deg,rgba(76,201,240,.18),rgba(139,92,246,.08));}
.layer.l2{background:linear-gradient(100deg,rgba(199,125,255,.18),rgba(139,92,246,.07));}
.layer.l3{background:linear-gradient(100deg,rgba(139,92,246,.24),rgba(76,201,240,.06));border-color:var(--violet);}
.layer.l4{background:linear-gradient(100deg,rgba(139,92,246,.14),rgba(8,12,26,.18));}
.layer.l5{background:linear-gradient(100deg,rgba(231,184,92,.14),rgba(8,12,26,.26));}
.layer .star{position:absolute;right:12px;top:9px;font-size:11px;color:var(--cyan);}
.bg-text{flex:1;display:flex;flex-direction:column;justify-content:center;overflow-y:auto;}
.bg-text h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:3px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:16px;}
.bg-text p{font-size:14.5px;color:var(--text-dim);line-height:1.95;max-width:640px;}
/* ===== 规则 ===== */
#rules{flex-direction:column;gap:0;}
.rules-scroll{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:22px;padding-right:8px;}
.rule-block h3{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;color:#fff;letter-spacing:2px;margin-bottom:6px;}
.rule-intro{font-size:13.5px;color:var(--text-dim);margin-bottom:12px;line-height:1.7;max-width:780px;}
.tier-list{display:flex;flex-direction:column;gap:8px;}
.tier-row{display:grid;grid-template-columns:96px 116px 1fr;gap:12px;align-items:center;background:var(--card);border:1px solid var(--card-bd);border-radius:10px;padding:10px 14px;transition:.2s;}
.tier-row:hover{border-color:var(--violet);background:rgba(139,92,246,.10);}
.tier-row .tn{font-family:"Press Start 2P",monospace;font-size:10px;color:var(--cyan);}
.tier-row .tname{font-size:15px;color:#fff;font-weight:500;}
.tier-row .tdesc{font-size:13px;color:var(--text-dim);line-height:1.5;}
/* ===== 图鉴 ===== */
#codex{padding:28px 48px;gap:0;}
.codex-wrap{display:flex;flex-direction:column;gap:13px;flex:1;min-height:0;}
.codex-cats{display:flex;gap:6px;flex-wrap:wrap;}
.codex-cats button{font-family:"Noto Sans SC",sans-serif;font-size:14px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:8px 15px;border-radius:11px;cursor:pointer;transition:.2s;}
.codex-cats button:hover{color:#fff;}
.codex-cats button.active{color:#fff;background:rgba(139,92,246,.24);border-color:var(--magenta);box-shadow:var(--glow);}
.codex-subrow{display:flex;align-items:center;gap:12px;}
.codex-subs{display:flex;gap:6px;flex-wrap:wrap;flex:1;}
.codex-subs button{font-size:12px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:5px 12px;border-radius:14px;cursor:pointer;transition:.2s;}
.codex-subs button:hover{color:#fff;}
.codex-subs button.active{color:#fff;background:rgba(76,201,240,.22);border-color:var(--cyan);}
.codex-search{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:18px;color:var(--text);padding:7px 14px;font-size:13px;outline:none;width:200px;flex:0 0 auto;}
.codex-search:focus{border-color:var(--violet);}
.codex-count{font-size:12px;color:var(--text-dim);white-space:nowrap;flex:0 0 auto;}
.codex-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;overflow-y:auto;padding:4px 6px 4px 0;align-content:start;flex:1;min-height:0;}
.cx{background:var(--card);border:1px solid var(--card-bd);border-radius:13px;overflow:hidden;transition:.24s;display:flex;flex-direction:column;cursor:default;}
.cx:hover{transform:translateY(-3px);border-color:var(--violet);box-shadow:var(--glow);}
.cx .thumb{width:100%;aspect-ratio:1/1;display:block;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.10));}
.cx .thumb svg{width:100%;height:100%;display:block;}
.cx .body{padding:10px 12px;}
.cx .nm{font-size:14.5px;color:#fff;font-weight:500;}
.cx .meta{display:flex;justify-content:space-between;margin-top:5px;font-size:11px;}
.cx .meta .sub{color:var(--cyan);}
.cx .meta .tier{color:var(--gold);}
.cx .desc{font-size:12px;color:var(--text-dim);line-height:1.5;margin-top:4px;max-height:60px;overflow:hidden;}
.cx .ds{font-size:11.5px;color:var(--text-dim);line-height:1.5;margin-top:6px;}
/* ===== 论坛 ===== */
#forum{flex-direction:row;gap:28px;}
.f-cats{flex:0 0 200px;display:flex;flex-direction:column;gap:12px;}
.f-cats .ch{font-size:15.5px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:13px 18px;border-radius:12px;cursor:pointer;transition:.22s;letter-spacing:.5px;}
.f-cats .ch:hover{color:#fff;background:rgba(139,92,246,.12);}
.f-cats .ch.active{color:#fff;background:linear-gradient(120deg,rgba(139,92,246,.28),rgba(199,125,255,.16));border-color:var(--magenta);box-shadow:var(--glow);}
.f-main{flex:1;display:flex;flex-direction:column;min-width:0;}
.threads{flex:1;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding-right:8px;align-content:start;}
.th{display:flex;align-items:center;gap:16px;background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:16px 20px;transition:.22s;cursor:pointer;}
.th:hover{border-color:var(--violet);background:rgba(139,92,246,.12);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.25);}
.th .pin{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.16);border:1px solid rgba(231,184,92,.45);border-radius:6px;padding:2px 8px;}
.th .tt{flex:1;font-size:16.5px;color:#fff;letter-spacing:.3px;}
.th .meta{font-size:13px;color:var(--text-dim);white-space:nowrap;}
.th .rp{color:var(--cyan);font-weight:600;}
.post-form{display:none;flex-direction:column;gap:10px;background:var(--card);border:1px solid var(--magenta);border-radius:14px;padding:16px;margin-bottom:14px;}
.post-form.show{display:flex;}
.post-form input,.post-form textarea{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:10px 14px;font-family:inherit;font-size:14px;outline:none;}
.post-form textarea{resize:vertical;min-height:72px;}
.post-form .row{display:flex;gap:10px;justify-content:flex-end;}
.post-form .row button{font-size:14px;padding:8px 18px;border-radius:10px;cursor:pointer;border:1px solid var(--card-bd);background:var(--card);color:var(--text);}
.post-form .row .ok{background:linear-gradient(120deg,var(--gold),#f0d49a);color:#0b1020;font-weight:700;border:none;}
/* ===== 公告 ===== */
#announce{gap:0;align-items:stretch;}
#announce .notes{flex:1;min-height:0;width:100%;display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:18px;overflow-y:auto;padding-right:8px;align-content:start;}
#announce .notes{grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:20px;}
.note{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.04));border:1px solid var(--card-bd);border-left:4px solid var(--magenta);border-radius:16px;transition:.24s;}
.note:hover{transform:translateY(-3px);border-color:var(--violet);background:linear-gradient(135deg,rgba(139,92,246,.16),rgba(76,201,240,.07));box-shadow:var(--glow);}
.note.pin{border-left-color:var(--gold);background:linear-gradient(135deg,rgba(231,184,92,.12),rgba(139,92,246,.06));}
.note.pin:hover{background:linear-gradient(135deg,rgba(231,184,92,.18),rgba(139,92,246,.08));}
.note.feat{grid-column:1 / -1;flex-direction:row;align-items:stretch;}
.note.feat .note-cover{flex:0 0 44%;width:44%;min-height:260px;}
.note.feat .note-body{flex:1;justify-content:center;}
.note.feat .note-title{font-size:27px;}
.note.feat .body{font-size:16px;}
.note-cover{height:168px;flex:0 0 auto;background-size:cover;background-position:center;background-repeat:no-repeat;border-bottom:1px solid var(--card-bd);position:relative;}
.note-cover.empty{background:linear-gradient(135deg,rgba(139,92,246,.24),rgba(76,201,240,.10));}
.note-cover.empty::after{content:'封面图 · 后台配置';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;letter-spacing:2px;color:rgba(255,255,255,.5);}
.note-body{padding:18px 22px;display:flex;flex-direction:column;min-width:0;}
.note .date{font-size:13px;font-weight:600;color:var(--cyan);letter-spacing:1px;margin-bottom:8px;}
.note-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:21px;color:#fff;letter-spacing:1px;margin-bottom:10px;line-height:1.32;text-shadow:0 0 14px rgba(139,92,246,.4);}
.note .body{font-size:15px;color:var(--text);line-height:1.9;}
.note .body p{margin:0 0 11px;}
.note .body p:last-child{margin-bottom:0;}
/* ===== 致玩家 ===== */
#letter{align-items:center;justify-content:center;}
#letter .letter-wrap{max-width:680px;width:100%;overflow-y:auto;padding:10px 20px;}
#letter .letter-wrap h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:4px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.5);margin-bottom:6px;text-align:center;}
#letter .letter-wrap .lt-sub{text-align:center;font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;margin-bottom:28px;}
#letter .letter-wrap .lt-body{font-size:15px;color:var(--text-dim);line-height:2.1;}
#letter .letter-wrap .lt-body p{margin-bottom:16px;}
#letter .letter-wrap .lt-body p:first-child{font-size:17px;color:#fff;font-weight:500;}
#letter .letter-wrap .lt-body .hl{color:var(--gold);font-weight:500;}
#letter .letter-wrap .lt-sign{text-align:right;margin-top:28px;font-size:14px;color:var(--magenta);font-style:italic;}
/* ===== 论坛(竖排:顶部分类条 + 下方纵向帖子流) ===== */
#forum{flex-direction:column;gap:20px;}
.f-cats{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;gap:10px;}
.f-cats .ch{flex:0 0 auto;}
.f-main{flex:1;min-height:0;}
.threads{width:100%;}
/* ===== 公告(内容居中) ===== */
#announce .notes{margin:0;}
/* ===== 剧情(小说阅读器:左目录 + 右正文,竖排阅读感) ===== */
#story{padding:28px 48px;}
.story-wrap{display:flex;flex-direction:row;width:100%;flex:1;min-height:0;}
.story-toc{flex:0 0 286px;display:flex;flex-direction:column;border-right:1px solid var(--card-bd);background:rgba(8,12,26,.35);min-height:0;}
.toc-head{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:24px;letter-spacing:3px;color:#fff;padding:24px 0 12px;text-shadow:0 0 16px rgba(139,92,246,.5);}
.toc-head small{display:block;font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;margin-top:6px;}
.toc-list{flex:1;overflow-y:auto;padding:0 12px 24px;}
.toc-loading{padding:30px 16px;color:var(--text-dim);font-size:14px;}
.toc-vol{margin-top:10px;}
.toc-vol .vname{font-size:14px;color:var(--gold);letter-spacing:1px;padding:8px 10px 4px;font-weight:500;}
.toc-vol .vintro{font-size:12px;color:var(--text-dim);line-height:1.6;padding:0 10px 8px;}
.toc-ch{display:block;width:100%;text-align:left;font-family:inherit;font-size:15px;color:var(--text-dim);background:transparent;border:none;border-left:2px solid transparent;padding:10px 12px;cursor:pointer;transition:.18s;border-radius:0 8px 8px 0;line-height:1.45;}
.toc-ch:hover{color:#fff;background:rgba(139,92,246,.12);}
.toc-ch.active{color:#fff;background:rgba(139,92,246,.20);border-left-color:var(--magenta);}
.story-reader{flex:1;overflow-y:auto;min-width:0;background:linear-gradient(180deg, rgba(139,92,246,.04), transparent 240px);}
.reader-loading{padding:80px 20px;text-align:center;color:var(--text-dim);font-size:15px;}
.reader-inner{max-width:840px;margin:0 auto;padding:48px 52px 100px;}
.reader-inner .r-vol{font-size:12px;color:var(--cyan);letter-spacing:2px;margin-bottom:10px;}
.reader-inner h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:32px;letter-spacing:2px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:18px;line-height:1.3;}
.reader-inner .body{font-size:17.5px;color:#eef2fb;line-height:2.0;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;}
.reader-inner .body p{margin-bottom:20px;text-indent:2em;}
.reader-inner .body p:first-child{text-indent:0;}
.reader-inner .body h2,.reader-inner .body h3{color:#fff;margin:22px 0 10px;font-size:18px;}
/* ===== 世界数据报表 ===== */
#worlddata{overflow:hidden;padding:28px 48px;}
#worldDataContent{flex:1;min-height:0;overflow-y:auto;width:100%;padding-right:8px;}
.wd-tag{display:inline-block;font-size:12px;letter-spacing:2px;color:var(--gold);background:rgba(231,184,92,.12);border:1px solid rgba(231,184,92,.38);border-radius:6px;padding:3px 11px;margin-bottom:22px;}
.wd-h{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;letter-spacing:3px;color:#fff;margin:38px 0 18px;text-shadow:0 0 14px rgba(139,92,246,.45);display:flex;align-items:center;gap:10px;}
.wd-h::before{content:"";width:4px;height:22px;border-radius:3px;background:linear-gradient(180deg,var(--violet),var(--cyan));box-shadow:0 0 10px rgba(139,92,246,.6);}
.wd-h small{font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;}
.wd-kpi{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;}
.wd-kpi-card{position:relative;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.12),rgba(76,201,240,.05));border:1px solid var(--card-bd);border-radius:16px;padding:22px 26px;transition:.26s;}
.wd-kpi-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);}
.wd-kpi-card::after{content:"";position:absolute;right:-26px;top:-26px;width:88px;height:88px;border-radius:50%;background:radial-gradient(circle,rgba(199,125,255,.22),transparent 70%);}
.wd-kpi-ic{font-size:22px;filter:drop-shadow(0 0 7px rgba(139,92,246,.55));}
.wd-kpi-num{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:40px;line-height:1.1;color:#fff;margin:10px 0 4px;}
.wd-kpi-num .u{font-size:14px;color:var(--text-dim);margin-left:4px;letter-spacing:1px;}
.wd-kpi-label{font-size:13px;color:var(--text-dim);letter-spacing:1px;}
.wd-kpi-delta{position:absolute;right:14px;top:16px;font-size:11px;font-weight:700;padding:1px 7px;border-radius:5px;}
.wd-up{color:#7CFC9B;background:rgba(124,252,155,.12);}
.wd-down{color:#ff8585;background:rgba(255,133,133,.12);}
.wd-bars{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px 26px;}
.wd-bar-row{margin-bottom:4px;}
.wd-bar-top{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:6px;}
.wd-bar-top .k{color:var(--text);}
.wd-bar-top .v{color:var(--gold);font-weight:700;}
.wd-track{height:9px;border-radius:6px;background:rgba(255,255,255,.07);overflow:hidden;}
.wd-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cyan),var(--violet));box-shadow:0 0 9px rgba(139,92,246,.55);}
.wd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:14px;background:rgba(255,255,255,.02);border:1px solid var(--card-bd);border-radius:14px;overflow:hidden;}
.wd-table th{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:14px;letter-spacing:2px;color:#fff;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.12));text-align:left;padding:13px 16px;border-bottom:1px solid var(--card-bd);}
.wd-table td{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06);color:var(--text-dim);line-height:1.5;}
.wd-table tr:last-child td{border-bottom:none;}
.wd-table tr:hover td{background:rgba(139,92,246,.08);color:#eaf0ff;}
.wd-table .lead{color:#fff;font-weight:600;}
.wd-table .pill{display:inline-block;font-size:12px;padding:2px 9px;border-radius:20px;background:rgba(76,201,240,.14);color:var(--cyan);border:1px solid rgba(76,201,240,.34);}
.wd-rank{display:flex;flex-direction:column;gap:12px;}
.wd-rank-row{display:flex;align-items:center;gap:14px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,255,255,.02));border:1px solid var(--card-bd);border-radius:13px;padding:12px 16px;transition:.22s;}
.wd-rank-row:hover{border-color:var(--violet);transform:translateX(4px);}
.wd-rank-no{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;width:34px;text-align:center;color:var(--gold);flex:0 0 auto;}
.wd-rank-row:nth-child(1) .wd-rank-no{color:#ffd56b;}
.wd-rank-row:nth-child(2) .wd-rank-no{color:#cfd8e8;}
.wd-rank-row:nth-child(3) .wd-rank-no{color:#e0a86b;}
.wd-rank-main{flex:1;min-width:0;}
.wd-rank-name{font-size:15px;color:#fff;margin-bottom:5px;display:flex;justify-content:space-between;}
.wd-rank-name .s{color:var(--gold);font-weight:700;font-size:13px;}
.wd-rank-track{height:8px;border-radius:5px;background:rgba(255,255,255,.07);overflow:hidden;}
.wd-rank-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--magenta),var(--violet));box-shadow:0 0 8px rgba(199,125,255,.5);}
@media (max-width:900px){
.grid{grid-template-columns:repeat(2,1fr);}
#background,#forum{flex-direction:column;overflow-y:auto;}
.layers,.f-cats{flex:none;}
#story .story-wrap{flex-direction:column;overflow-y:auto;}
.story-toc{flex:none;border-right:none;border-bottom:1px solid var(--card-bd);}
.toc-list{max-height:220px;}
.hero .copy h1{font-size:42px;}
.nav button{padding:8px 10px;font-size:13px;}
.nav{overflow-x:auto;scrollbar-width:none;}
.nav::-webkit-scrollbar{display:none;}
}
/* ===== 移动端适配(手机 / 平板竖屏) ===== */
@media (max-width:768px){
html,body{height:100%;}
body{height:100dvh;}
.app{height:100dvh;}
.topbar{height:auto;min-height:54px;flex-wrap:wrap;align-items:center;padding:8px 14px;gap:10px;}
.logo .cn{font-size:22px;letter-spacing:2px;}
.logo .en{display:none;}
.spacer{display:none;}
.user-box{margin-left:auto;font-size:13px;gap:8px;}
.user-box .auth-btn{padding:6px 13px;font-size:12px;}
.nav{order:3;flex:1 0 100%;width:100%;margin-left:0;gap:4px;padding-top:6px;border-top:1px solid var(--card-bd);}
.nav button{font-size:13px;padding:7px 9px;white-space:nowrap;}
.hero .copy{left:18px;right:18px;bottom:82px;}
.hero .copy{left:18px;top:28px;}
.hero .copy h1{font-size:32px;letter-spacing:4px;}
.hero .copy .tag{font-size:16px;}
.slide-label{left:18px;bottom:78px;}
.slide-label .sl-cn{font-size:24px;letter-spacing:3px;}
.slide-label .sl-desc{font-size:13px;max-width:100%;}
.sl-dots{right:16px;gap:12px;}
.hero .enter-wrap{bottom:28px;}
.hero .enter-btn{font-size:15px;padding:11px 30px;}
.panel{padding:18px 18px;}
.bg-page-title{font-size:30px;}
.bg-page-body{font-size:15.5px;line-height:1.9;}
.grid{grid-template-columns:repeat(2,1fr);gap:12px;}
.card{min-height:120px;padding:15px 16px;}
.codex-search{width:140px;}
.reader-inner{padding:26px 18px 80px;}
.reader-inner h1{font-size:23px;}
#worlddata{padding:28px 16px 80px;}
#background{padding:30px 16px 80px;}
.story-toc{flex-basis:auto;}
#story{padding:18px 18px;}
.tier-row{grid-template-columns:72px 92px 1fr;gap:8px;}
.note.feat{flex-direction:column;}
.note.feat .note-cover{flex:0 0 auto;width:100%;min-height:180px;}
#announce .notes{grid-template-columns:1fr;}
}
@media (max-width:480px){
.topbar{padding:8px 10px;gap:8px;}
.logo .cn{font-size:20px;}
.nav button{font-size:12px;padding:6px 7px;}
.user-box .auth-btn{padding:6px 11px;}
.hero .copy{left:14px;top:20px;}
.hero .copy h1{font-size:26px;letter-spacing:2px;}
.hero .copy .tag{font-size:14px;}
.slide-label{left:14px;bottom:70px;}
.slide-label .sl-cn{font-size:20px;}
.slide-label .sl-desc{font-size:12.5px;}
.sl-dots{right:12px;gap:10px;}
.grid{grid-template-columns:1fr;}
.panel{padding:14px 14px;}
.codex-search{width:118px;flex:1 1 100%;}
.codex-subrow{flex-wrap:wrap;}
.reader-inner{padding:22px 14px 70px;}
#story{padding:14px 14px;}
.tier-row{grid-template-columns:60px 76px 1fr;gap:6px;}
.codex-cats button,.codex-subs button{padding:7px 11px;font-size:13px;}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="logo" id="logoHome"><span class="cn">宇森</span></div>
<nav class="nav">
<button data-tab="background">背景</button>
<button data-tab="rules">规则</button>
<button data-tab="codex">图鉴</button>
<button data-tab="forum" class="auth-only">论坛</button>
<button data-tab="announce">公告</button>
<button data-tab="story">剧情</button>
<button data-tab="worlddata" class="auth-only">世界数据</button>
</nav>
<div class="spacer"></div>
<div class="user-box" id="userBox"></div>
</header>
<main class="content">
<!-- ===== 介绍(视频主页) ===== -->
<section class="panel active" id="intro">
<div class="hero">
<div class="slideshow" id="slideshow">
<!-- 五层世界轮播,由 JS 渲染 -->
</div>
<div class="enter-wrap">
<button class="enter-btn" id="enterGame">进入游戏</button>
</div>
</div>
</section>
<!-- ===== 背景(特色 + 世界背景 + 致玩家 合并平铺) ===== -->
<section class="panel" id="background">
<div class="panel-scroll" id="bgBody"></div>
</section>
<!-- ===== 规则 ===== -->
<section class="panel" id="rules">
<div class="rules-scroll">
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<!-- 如页面此处为空白 = Odoo API 未返回规则数据 -->
</div>
</section>
<!-- ===== 图鉴 ===== -->
<section class="panel" id="codex">
<div class="codex-wrap">
<div class="codex-cats" id="codexCats"></div>
<div class="codex-subrow">
<div class="codex-subs" id="codexSubs"></div>
<input class="codex-search" id="codexSearch" placeholder="搜索名称 / 描述…" />
<span class="codex-count" id="codexCount"></span>
</div>
<div class="codex-grid" id="codexGrid"></div>
</div>
</section>
<!-- ===== 论坛 ===== -->
<section class="panel" id="forum">
<div class="f-cats">
<!-- 静态分类已注释 — 验证 API 是否取到后台数据 -->
<!-- 如页面此处为空白 = Odoo API 未返回论坛分类 -->
</div>
<div class="f-main">
<div class="post-form" id="postForm">
<input id="pfTitle" placeholder="标题" />
<textarea id="pfBody" placeholder="说点什么…(演示:仅本次会话内可见)"></textarea>
<div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div>
</div>
<div class="threads" id="threads">
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<!-- 如页面此处为空白 = Odoo API 未返回论坛数据 -->
</div>
</div>
</section>
<!-- ===== 公告 ===== -->
<section class="panel" id="announce">
<div class="notes">
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
<!-- 如页面此处为空白 = Odoo API 未返回公告数据 -->
</div>
</section>
<!-- ===== 剧情(小说阅读器:左目录 + 右正文) ===== -->
<section class="panel" id="story">
<div class="story-wrap">
<aside class="story-toc">
<div class="toc-head">目录<small>CONTENTS</small></div>
<div class="toc-list" id="storyTocList"><div class="toc-loading">加载中…</div></div>
</aside>
<article class="story-reader" id="storyReader">
<div class="reader-loading">从左侧目录选择章节开始阅读</div>
</article>
</div>
</section>
<!-- ===== 世界数据(登录后可见) ===== -->
<section class="panel" id="worlddata">
<div id="worldDataContent"></div>
</section>
</main>
</div>
<script>
// top nav
const navBtns=document.querySelectorAll('.nav button');
const panels=document.querySelectorAll('.panel');
navBtns.forEach(b=>b.addEventListener('click',()=>{
navBtns.forEach(x=>x.classList.remove('active'));
panels.forEach(p=>p.classList.remove('active'));
b.classList.add('active');
document.getElementById(b.dataset.tab).classList.add('active');
}));
// logo 点击回主页intro 面板)
document.getElementById('logoHome').addEventListener('click',()=>{
navBtns.forEach(x=>x.classList.remove('active'));
panels.forEach(p=>p.classList.remove('active'));
document.getElementById('intro').classList.add('active');
});
// ============ 五层世界图片轮播 ============
(function(){
const slides = [
{ img:'assets/slides/01_starry_sky.png', en:'STARRY SKY', cn:'星空', desc:'陨石星空带悬浮于世界最外层,是文明触及维度的起点。星轨连通各浮空岛,穹鎏晶体自星尘中凝结。',
origin:'世界未形,星尘先于万物浮游于虚无。首缕维度自星隙坍缩成形,穹鎏晶体由尘凝就,文明自此仰望苍穹之始。' },
{ img:'assets/slides/02_sky.png', en:'SKY', cn:'天空', desc:'云朵地基块托起浮空岛群,灵气复苏的第一缕风拂过。各族初代聚落,在此仰望也更在此扎根。',
origin:'云基初结,浮空岛群自苍穹垂落凡尘。灵气复苏,初民循风而居,于云上筑城,仰望亦扎根,道统由此萌发。' },
{ img:'assets/slides/03_earth.png', en:'EARTH', cn:'大地', desc:'平原与森林铺展成万物生灵的摇篮,玄荒文明于斯鼎盛。城邦林立,征伐与道统在沃土上并行。',
origin:'沃野铺展为万灵摇篮,玄荒先族自林泉走出。城邦并起,征伐与道统并驰沃土之上,文明于此鼎盛绵延。' },
{ img:'assets/slides/04_underground.png',en:'UNDERGROUND',cn:'地下', desc:'溶洞晶矿与暗河在深处低语,冥玉生于幽暗玉石之中。地心工坊调度浅层资源,灯火不熄。',
origin:'溶洞深开,晶矿与暗河在幽处低语。冥玉生于幽玉深心,地心工坊取浅层之资,灯火长明,匠魂不熄。' },
{ img:'assets/slides/05_core.png', en:'CORE', cn:'地心', desc:'熔岩奔流的地心,是世界诞生的本源。元玺在此凝聚,追溯天地初开的那一瞬永恒。',
origin:'熔岩奔流处,乃世界本源所钟。元玺于亿载炽火中凝聚成形,溯天地初开一瞬,万物归处,亦是起始。' }
];
const container = document.getElementById('slideshow');
if(!container) return;
let current = 0, timer = null;
const INTERVAL = 5500;
// 竖向轨道5 张图上下滑动
const track = document.createElement('div');
track.className='slide-track';
container.appendChild(track);
// 渲染 slide + label
slides.forEach((s,i)=>{
const el = document.createElement('div');
el.className='slide'+(i===0?' active':'');
el.style.backgroundImage="url('"+s.img+"')";
el.innerHTML='<div class="slide-label"><span class="sl-en">'+s.en+'</span><div class="sl-cn">'+s.cn+'</div><div class="sl-desc">'+s.desc+'</div></div>'
+'<div class="slide-origin"><span class="so-tag">起源</span>'+s.origin+'</div>';
track.appendChild(el);
});
const all = track.querySelectorAll('.slide');
// 指示器
const dotsWrap = document.createElement('div');
dotsWrap.className='sl-dots';
slides.forEach((_,i)=>{
const d=document.createElement('div');
d.className='sl-dot'+(i===0?' active':'');
d.addEventListener('click',()=>go(i));
dotsWrap.appendChild(d);
});
document.querySelector('.hero').appendChild(dotsWrap);
const dots = dotsWrap.querySelectorAll('.sl-dot');
function go(idx){
if(idx<0)idx=slides.length-1;if(idx>=slides.length)idx=0;
if(idx===current) return;
all[current].classList.remove('active');dots[current].classList.remove('active');
current=idx;
track.style.transform='translateY('+(-current*100)+'%)';
all[current].classList.add('active');dots[current].classList.add('active');
resetTimer();
}
function next(){ go((current+1)%slides.length); }
function prev(){ go((current-1+slides.length)%slides.length); }
function resetTimer(){ clearInterval(timer); timer=setInterval(next,INTERVAL); }
// 鼠标滚轮上下滑动
let lock=false,wAccum=0;
document.querySelector('.hero').addEventListener('wheel',function(e){
wAccum+=e.deltaY;
if(lock)return;
if(Math.abs(wAccum)>50){
lock=true;
if(wAccum>0)next();else prev();
wAccum=0;
setTimeout(function(){lock=false;},1250);
}
},{passive:true});
resetTimer();
})();
// ============ 程序化矢量缩略图(零 AI 生图,不消耗积分) ============
function hashStr(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return h>>>0;}
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
function starPts(cx,cy,spikes,outer,inner){
let pts=[],rot=-Math.PI/2;
for(let i=0;i<spikes*2;i++){const rad=i%2?inner:outer;const a=rot+i*Math.PI/spikes;pts.push((cx+Math.cos(a)*rad).toFixed(1)+','+(cy+Math.sin(a)*rad).toFixed(1));}
return pts.join(' ');
}
function polygonPts(cx,cy,sides,R){
let pts=[];for(let i=0;i<sides;i++){const a=-Math.PI/2+i*2*Math.PI/sides;pts.push((cx+Math.cos(a)*R).toFixed(1)+','+(cy+Math.sin(a)*R).toFixed(1));}
return pts.join(' ');
}
// 各品类图形生成器seed 用于唯一渐变 idr 为确定性随机)
function shRace(r,seed){
const v=Math.floor(r()*3);
let s=`<ellipse cx="50" cy="55" rx="19" ry="21" fill="url(#fg${seed})" stroke="rgba(255,255,255,.25)" stroke-width="1"/>`;
if(v===0){s+=`<path d="M35 40 L31 20 L43 36 Z" fill="url(#fg${seed})"/><path d="M65 40 L69 20 L57 36 Z" fill="url(#fg${seed})"/>`;}
else if(v===1){s+=`<path d="M32 52 L20 42 L36 46 Z" fill="url(#fg${seed})"/><path d="M68 52 L80 42 L64 46 Z" fill="url(#fg${seed})"/>`;}
else{s+=`<path d="M34 36 L42 24 L50 34 L58 24 L66 36 Z" fill="url(#fg${seed})"/>`;}
s+=`<circle cx="43" cy="55" r="2.6" fill="#0b1020"/><circle cx="57" cy="55" r="2.6" fill="#0b1020"/>`;
return s;
}
function shBuild(r,seed){
let s=`<rect x="34" y="44" width="32" height="34" rx="2" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<path d="M30 44 L50 28 L70 44 Z" fill="url(#fg${seed})"/>`;
for(let i=0;i<3;i++)s+=`<rect x="${39+i*7}" y="52" width="4" height="7" rx="1" fill="rgba(8,12,26,.7)"/>`;
return s;
}
function shElement(r,seed){
const sides=3+Math.floor(r()*6);
let s=`<polygon points="${polygonPts(50,52,sides,25)}" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<circle cx="50" cy="52" r="7" fill="url(#fg${seed})"/>`;
return s;
}
function shPersona(r,seed){
let s=`<path d="M32 36 Q50 28 68 36 Q72 56 50 70 Q28 56 32 36 Z" fill="url(#fg${seed})" opacity=".9"/>`;
s+=`<circle cx="43" cy="48" r="2.8" fill="#0b1020"/><circle cx="57" cy="48" r="2.8" fill="#0b1020"/>`;
s+=`<path d="M43 60 Q50 64 57 60" stroke="#0b1020" stroke-width="2" fill="none"/>`;
return s;
}
function shTalent(r,seed){
const sp=4+Math.floor(r()*4);
let s=`<polygon points="${starPts(50,52,sp,26,11)}" fill="url(#fg${seed})" opacity=".92"/>`;
s+=`<circle cx="50" cy="52" r="5" fill="rgba(8,12,26,.55)"/>`;
return s;
}
function shRelic(r,seed){
let s=`<path d="M50 24 L70 46 L50 76 L30 46 Z" fill="url(#fg${seed})" opacity=".9" stroke="rgba(255,255,255,.3)" stroke-width="1"/>`;
s+=`<path d="M30 46 L70 46 M50 24 L42 46 L50 76 M50 24 L58 46 L50 76" stroke="rgba(8,12,26,.4)" stroke-width="1" fill="none"/>`;
return s;
}
function shEquip(sub,seed){
switch(sub){
case 'head': return `<path d="M32 56 Q32 30 50 30 Q68 30 68 56 L68 60 L32 60 Z" fill="url(#fg${seed})"/><rect x="44" y="44" width="12" height="5" rx="2" fill="rgba(8,12,26,.6)"/>`;
case 'hand': {let s=`<rect x="36" y="40" width="28" height="22" rx="6" fill="url(#fg${seed})"/><rect x="33" y="44" width="9" height="14" rx="3" fill="url(#fg${seed})"/>`;for(let i=0;i<3;i++)s+=`<rect x="${40+i*8}" y="62" width="5" height="8" rx="2" fill="url(#fg${seed})"/>`;return s;}
case 'chest': return `<path d="M30 46 Q50 38 70 46 L70 66 Q50 72 30 66 Z" fill="url(#fg${seed})"/><rect x="47" y="46" width="6" height="18" fill="rgba(8,12,26,.5)"/>`;
case 'pants': return `<path d="M38 32 L62 32 L60 74 L52 74 L50 50 L48 74 L40 74 Z" fill="url(#fg${seed})"/>`;
case 'shoes': return `<rect x="34" y="44" width="28" height="6" rx="3" fill="url(#fg${seed})"/><path d="M34 48 L60 48 L66 64 L34 64 Z" fill="url(#fg${seed})"/>`;
case 'ring': return `<circle cx="50" cy="55" r="16" fill="none" stroke="url(#fg${seed})" stroke-width="5"/><path d="M50 29 L57 40 L43 40 Z" fill="url(#fg${seed})"/>`;
case 'neck': return `<path d="M32 42 Q50 64 68 42" fill="none" stroke="url(#fg${seed})" stroke-width="3"/><path d="M44 58 L50 72 L56 58 Z" fill="url(#fg${seed})"/>`;
case 'treasure': return `<rect x="38" y="60" width="24" height="9" rx="2" fill="url(#fg${seed})"/><circle cx="50" cy="46" r="14" fill="url(#fg${seed})" opacity=".9"/><circle cx="50" cy="46" r="6" fill="rgba(255,255,255,.4)"/>`;
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function shEvent(r,seed){
let s=`<ellipse cx="50" cy="52" rx="22" ry="28" fill="none" stroke="url(#fg${seed})" stroke-width="3"/>`;
s+=`<ellipse cx="50" cy="52" rx="12" ry="16" fill="url(#fg${seed})" opacity=".5"/>`;
return s;
}
function shapeFor(cat,sub,r,seed){
switch(cat){
case 'race': return shRace(r,seed);
case 'build': return shBuild(r,seed);
case 'element': return shElement(r,seed);
case 'persona': return shPersona(r,seed);
case 'talent': return shTalent(r,seed);
case 'relic': return shRelic(r,seed);
case 'equip': return shEquip(sub,seed);
case 'event': return shEvent(r,seed);
default: return `<circle cx="50" cy="50" r="18" fill="url(#fg${seed})"/>`;
}
}
function thumbSVG(it){
const seed=hashStr(it.cat+it.sub+it.name);
const r=rng(seed);
const h=Math.floor(r()*360);
const h2=(h+40)%360;
const tcol=TIERCOL[it.tier]||'#9aa6c4';
const shape=shapeFor(it.cat,it.sub,r,seed);
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">`+
`<defs>`+
`<radialGradient id="bg${seed}" cx="50%" cy="36%" r="78%">`+
`<stop offset="0%" stop-color="hsl(${h},48%,26%)"/>`+
`<stop offset="100%" stop-color="hsl(${h},42%,9%)"/>`+
`</radialGradient>`+
`<linearGradient id="fg${seed}" x1="0" y1="0" x2="0" y2="1">`+
`<stop offset="0%" stop-color="hsl(${h},72%,66%)"/>`+
`<stop offset="100%" stop-color="hsl(${h2},70%,56%)"/>`+
`</linearGradient>`+
`</defs>`+
`<rect width="100" height="100" fill="url(#bg${seed})"/>`+
shape+
`<circle cx="50" cy="50" r="46" fill="none" stroke="${tcol}" stroke-width="2.5" opacity=".9"/>`+
`</svg>`;
}
// ============ 图鉴(数据驱动 + 程序化缩略图) ============
// ---- 数据源层 ----
// 当前: 内置框架样例 (window.CODEX)
// 将来: 从 Odoo 后台取数 (模型 game.codex.entry),经已代理的 /jsonrpc 读取
// 启用方式: 在 codex-data.js 之前设置 window.CODEX_SOURCE='odoo'
const CODEX_SOURCE = window.CODEX_SOURCE || 'odoo'; // 'embedded' | 'odoo'
async function loadCodex(){
if (CODEX_SOURCE === 'odoo') return await fetchCodexFromOdoo();
return window.CODEX;
}
// Odoo JSON-RPC 取数预留接口后台模型就绪后启用nginx 已代理 /jsonrpc -> Odoo:8069
async function fetchCodexFromOdoo(){
const r = await fetch('/game/api/codex?db=game', {credentials:'same-origin'});
const d = await r.json();
const ICON={'race':'👥','build':'🏗️','element':'🔮','persona':'🎭','talent':'⚡','relic':'💎','equip':'🛡️','event':'🌟'};
const groups = (d && d.groups) || [];
const categories = groups.map(g=>({id:g.key, name:g.label, icon:ICON[g.key]||'◆'}));
const subs={}; const items=[];
groups.forEach(g=>{(g.subs||[]).forEach(s=>{(subs[g.key]=subs[g.key]||[]).push(s.name);(s.items||[]).forEach(it=>items.push({cat:g.key, sub:it.sub, name:it.name, tier:it.tier, desc:it.desc}));});});
return { categories, subs, items };
}
(async function(){
const grid=document.getElementById('codexGrid');
const catBar=document.getElementById('codexCats');
const subBar=document.getElementById('codexSubs');
const search=document.getElementById('codexSearch');
const countEl=document.getElementById('codexCount');
const data = await loadCodex();
if(!data||!grid) return;
const {categories,subs,items}=data;
let curCat=categories[0].id, curSub='all', q='';
const iconOf=id=>(categories.find(c=>c.id===id)||{}).icon||'◆';
catBar.innerHTML=categories.map(c=>`<button data-cat="${c.id}" class="${c.id===curCat?'active':''}">${c.icon} ${c.name}</button>`).join('');
function renderSubs(){
const list=subs[curCat]||[];
subBar.innerHTML=`<button data-sub="all" class="${curSub==='all'?'active':''}">全部</button>`+
list.map(s=>`<button data-sub="${s}" class="${curSub===s?'active':''}">${s}</button>`).join('');
subBar.querySelectorAll('button').forEach(b=>b.onclick=()=>{curSub=b.dataset.sub;renderSubs();renderGrid();});
}
function renderGrid(){
let arr=items.filter(it=>it.cat===curCat&&(curSub==='all'||it.sub===curSub));
if(q) arr=arr.filter(it=>it.name.includes(q)||it.desc.includes(q));
countEl.textContent='共 '+arr.length+' 条';
grid.innerHTML=arr.map(it=>
`<div class="cx"><div class="thumb">${thumbSVG(it)}</div>`+
`<div class="body"><div class="nm">${it.name}</div>`+
`<div class="meta"><span class="sub">${it.sub}</span><span class="tier">${it.tier}</span></div>`+
`<div class="ds">${it.desc}</div></div></div>`).join('');
}
catBar.querySelectorAll('button').forEach(b=>b.onclick=()=>{
curCat=b.dataset.cat;curSub='all';
catBar.querySelectorAll('button').forEach(x=>x.classList.remove('active'));
b.classList.add('active');renderSubs();renderGrid();
});
search.oninput=e=>{q=e.target.value.trim();renderGrid();};
renderSubs();renderGrid();
})();
// forum category
const fCats=document.querySelectorAll('.f-cats .ch');
const threads=document.querySelectorAll('#threads .th');
fCats.forEach(b=>b.addEventListener('click',()=>{
fCats.forEach(x=>x.classList.remove('active'));
b.classList.add('active');
const c=b.dataset.fcat;
threads.forEach(t=>{ t.style.display=(c==='all'||t.dataset.fcat===c)?'':'none'; });
}));
// forum post (in-session demo)
const postForm=document.getElementById('postForm');
const newPostBtn=document.getElementById('newPost');
if(newPostBtn && postForm) newPostBtn.addEventListener('click',()=>postForm.classList.toggle('show'));
document.getElementById('pfCancel').addEventListener('click',()=>postForm.classList.remove('show'));
document.getElementById('pfOk').addEventListener('click',()=>{
const t=document.getElementById('pfTitle').value.trim();
if(!t)return;
const el=document.createElement('div');
el.className='th';el.dataset.fcat='discuss';
el.innerHTML='<span class="tt">'+t+'</span><span class="meta"><span class="rp">0</span> 回复 · 我</span>';
document.getElementById('threads').prepend(el);
document.getElementById('pfTitle').value='';
document.getElementById('pfBody').value='';
postForm.classList.remove('show');
});
// 登录态 / 进入游戏 逻辑见末尾 <script>(需 game-api.js 先加载)
</script>
<script src="assets/game-api.js"></script>
<script src="assets/site-app.js"></script>
<script>
// ============ 登录态 / 右上角个人信息(从 Odoo 取) ============
(function(){
const userBox = document.getElementById('userBox');
let cachedMe = null;
function renderLoggedOut(){
if(!userBox) return;
userBox.innerHTML = '<a href="login.html?next=world.html" class="auth-link">登录</a><a href="login.html?next=world.html" class="auth-btn">注册</a>';
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.remove('visible'));
}
function renderLoggedIn(me){
if(!userBox) return;
userBox.innerHTML = '<span class="ub-name"></span><button class="ub-logout" id="ubLogout">退出</button>';
const nm = userBox.querySelector('.ub-name');
if(nm) nm.textContent = me.name || me.login;
document.querySelectorAll('.nav .auth-only').forEach(el => el.classList.add('visible'));
const lb = document.getElementById('ubLogout');
if(lb) lb.addEventListener('click', async ()=>{
try { await API.logout(); } catch(e){}
location.href = 'index.html';
});
}
async function refreshUserBox(){
try{
cachedMe = await API.me();
if(cachedMe && cachedMe.uid) renderLoggedIn(cachedMe);
else renderLoggedOut();
}catch(e){ renderLoggedOut(); }
}
refreshUserBox();
// ============ 背景页:从 odoo 拉取页面板块渲染 ============
async function renderBackground(){
const box = document.getElementById('bgBody');
if(!box) return;
try{
const r = await fetch('/game/api/pages?db=game', {credentials:'same-origin'});
const d = await r.json();
const pages = (d && d.items) || [];
if(!pages.length){ box.innerHTML = '<div class="bg-empty">板块内容即将上线…</div>'; return; }
box.innerHTML = pages.map(p=>{
const cards = (p.items||[]).map(it=>`
<div class="bg-card">
<div class="bg-card-ic">${it.icon||'✦'}</div>
<div class="bg-card-body">
<div class="bg-card-title">${it.title||''}</div>
<div class="bg-card-desc">${it.desc||''}</div>
</div>
</div>`).join('');
return `
<section class="bg-page">
<div class="bg-page-head">
<h2 class="bg-page-title">${p.title||''}</h2>
${p.subtitle?`<div class="bg-page-sub">${p.subtitle}</div>`:''}
</div>
${p.body_html?`<div class="bg-page-body">${p.body_html}</div>`:''}
${cards?`<div class="bg-page-items">${cards}</div>`:''}
</section>`;
}).join('');
}catch(e){
box.innerHTML = '<div class="bg-empty">板块加载失败,请刷新重试。</div>';
}
}
renderBackground();
// ============ 世界数据:大报表(演示数据,后端接口待接入) ============
async function renderWorldData(){
const box = document.getElementById('worldDataContent');
if(!box) return;
// TODO: 后端接入后改为 await API.worldData() 取真实数据
const data = worldDataMock();
const kpis = data.kpi.map(k=>`
<div class="wd-kpi-card">
<span class="wd-kpi-delta ${k.delta>=0?'wd-up':'wd-down'}">${k.delta>=0?'▲':'▼'} ${Math.abs(k.delta)}%</span>
<div class="wd-kpi-ic">${k.icon}</div>
<div class="wd-kpi-num">${k.num}<span class="u">${k.unit||''}</span></div>
<div class="wd-kpi-label">${k.label}</div>
</div>`).join('');
const bars = data.bars.map(b=>`
<div class="wd-bar-row">
<div class="wd-bar-top"><span class="k">${b.k}</span><span class="v">${b.v}%</span></div>
<div class="wd-track"><div class="wd-fill" style="width:${b.v}%"></div></div>
</div>`).join('');
const rows = data.layers.map(l=>`
<tr>
<td class="lead">${l.lv}</td>
<td>${l.band}</td>
<td>${l.eco}</td>
<td>${l.pop}</td>
<td><span class="pill">${l.terr}</span></td>
</tr>`).join('');
const ranks = data.factions.map((f,i)=>`
<div class="wd-rank-row">
<div class="wd-rank-no">${i+1}</div>
<div class="wd-rank-main">
<div class="wd-rank-name"><span>${f.name}</span><span class="s">${f.score} 影响力</span></div>
<div class="wd-rank-track"><div class="wd-rank-fill" style="width:${f.pct}%"></div></div>
</div>
</div>`).join('');
box.innerHTML = `
<span class="wd-tag">演示数据 · 后端接口待接入</span>
<div class="wd-kpi">${kpis}</div>
<div class="wd-h">核心指标进度 <small>CORE METRICS</small></div>
<div class="wd-bars">${bars}</div>
<div class="wd-h">世界五层结构 <small>WORLD LAYERS</small></div>
<table class="wd-table">
<thead><tr><th>层级</th><th>高度带</th><th>地表生态</th><th>人口占比</th><th>代表地形</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<div class="wd-h">势力影响力排行 <small>FACTION RANKING</small></div>
<div class="wd-rank">${ranks}</div>
`;
}
function worldDataMock(){
return {
kpi:[
{icon:'👥',num:'12,840',unit:'人',label:'世界总人口',delta:3},
{icon:'🌍',num:'Lv.7',unit:'',label:'当前世界等级',delta:1},
{icon:'🏛️',num:'38',unit:'个',label:'活跃势力',delta:5},
{icon:'⛪',num:'9.6M',unit:'',label:'信仰值总量',delta:8},
{icon:'⚔️',num:'214',unit:'',label:'在线角色',delta:-2},
{icon:'💎',num:'1.3M',unit:'',label:'流通晶币',delta:4}
],
bars:[
{k:'世界安定度',v:78},
{k:'生态健康度',v:64},
{k:'信仰传播度',v:88},
{k:'文明探索度',v:52},
{k:'势力平衡度',v:71},
{k:'资源充裕度',v:60}
],
layers:[
{lv:'第一层 · 地表',band:'080m',eco:'平原 / 森林 / 河流',pop:'58%',terr:'草原城邦'},
{lv:'第二层 · 丘陵',band:'80260m',eco:'梯田 / 矿脉 / 林地',pop:'22%',terr:'山道驿站'},
{lv:'第三层 · 高地',band:'260600m',eco:'苔原 / 雪线 / 圣峰',pop:'12%',terr:'云端神殿'},
{lv:'第四层 · 地下',band:'-30-200m',eco:'溶洞 / 晶矿 / 暗河',pop:'6%',terr:'地心工坊'},
{lv:'第五层 · 秘境',band:'位面裂隙',eco:'虚空 / 混沌 / 星界',pop:'2%',terr:'裂隙哨站'}
],
factions:[
{name:'曦光同盟',score:9420,pct:100},
{name:'玄铁工盟',score:8730,pct:93},
{name:'碧波联邦',score:7610,pct:81},
{name:'夜枭议会',score:6880,pct:73},
{name:'荒原游商',score:5240,pct:56},
{name:'星语祭司团',score:4310,pct:46}
]
};
}
renderWorldData();
// ============ 进入游戏:已登录→世界页;未登录→登录页 ============
const enterBtn = document.getElementById('enterGame');
if(enterBtn) enterBtn.addEventListener('click', async ()=>{
try {
const me = cachedMe || await API.me();
if (me && me.uid) location.href = 'world.html';
else location.href = 'login.html?next=world.html';
} catch(e) {
location.href = 'login.html?next=world.html';
}
});
})();
</script>
</body>
</html>