feat: 官网导航重构 + 主世界交互优化

- 官网(index.html/site-app.js): 特色/致玩家并入背景页(平铺非子菜单), 新增剧情tab, 新增登录后可见的"世界数据"tab
- 主世界(world.html): 顶栏账号下拉常驻去重排, 坐标布局修正, 默认缩放下调, 微观滑动阴影修复, 时钟四色扇形+边框, 方向键/WASD平移+边界限位
- 官网百科页(website/intro.html): 背景页整合内容
This commit is contained in:
李鹏宇 2026-07-24 10:55:38 +08:00
parent c6ccc1d50b
commit 98a204b309
4 changed files with 323 additions and 143 deletions

View File

@ -1,43 +1,43 @@
// 宇森官网 · 公开页数据打通(图鉴/规则/论坛/公告 从 Odoo 取数)
// 依赖 assets/game-api.js
// ---------- 页面板块(特色 / 背景 / 致玩家) ----------
// ---------- 页面板块:特色 / 背景 / 致玩家 合并进「背景」单页(平铺,非子菜单) ----------
function featuresHTML(p){
const items = (p.items || []).map(it =>
`<div class="card"><div class="ic">${it.icon || ''}</div><h3>${it.title || ''}</h3><p>${it.desc || ''}</p></div>`
).join('');
return `<div class="sec-title">${p.title || '游戏特色'}<small>${p.subtitle || ''}</small></div><div class="grid">${items}</div>`;
}
function backgroundHTML(p){
const layers = (p.items || []).map((it, idx) =>
`<div class="layer l${idx + 1}"><div class="lt">${it.title || ''}</div><div class="ld">${it.desc || ''}</div><span class="star">${it.icon || '✦'}</span></div>`
).join('');
return `<div class="layers">${layers}</div><div class="bg-text"><h2>${p.title || '世界起源'}</h2>${p.body_html || ''}</div>`;
}
function letterHTML(p){
return `<div class="letter-wrap"><h2>${p.title || ''}</h2><div class="lt-sub">${p.subtitle || ''}</div><div class="lt-body">${p.body_html || ''}</div><div class="lt-sign">—— 宇森开发组</div></div>`;
}
async function loadPages(){
try {
const d = await API.pages();
const pages = (d && d.items) || [];
const bg = document.getElementById('background');
let html = '';
pages.forEach(p => {
if (p.key === 'features') renderFeatures(p);
else if (p.key === 'background') renderBackground(p);
else if (p.key === 'letter') renderLetter(p);
if (p.key === 'features') html += featuresHTML(p);
else if (p.key === 'background') html += backgroundHTML(p);
else if (p.key === 'letter') html += letterHTML(p);
});
if (bg) bg.innerHTML = html; // 特色 + 背景 + 致玩家 平铺进同一页,无子菜单切换
const story = document.getElementById('story');
if (story) story.innerHTML = '<div class="sec-title">剧情<small>STORY</small></div><div style="text-align:center;padding:80px 20px;color:#556677;font-size:15px;"><div style="font-size:48px;margin-bottom:16px;">📖</div>剧情内容正在筹备中…<div style="font-size:13px;margin-top:8px;color:#445566;">敬请期待</div></div>';
} catch (e) { /* 失败时保留 HTML 中的静态兜底内容 */ }
}
function renderFeatures(p){
const sec = document.getElementById('features');
if (!sec) return;
const items = (p.items || []).map(it =>
`<div class="card"><div class="ic">${it.icon || ''}</div><h3>${it.title || ''}</h3><p>${it.desc || ''}</p></div>`
).join('');
sec.innerHTML = `<div class="sec-title">${p.title || '游戏特色'}<small>${p.subtitle || ''}</small></div><div class="grid">${items}</div>`;
}
function renderBackground(p){
const sec = document.getElementById('background');
if (!sec) return;
const layers = (p.items || []).map((it, idx) =>
`<div class="layer l${idx + 1}"><div class="lt">${it.title || ''}</div><div class="ld">${it.desc || ''}</div><span class="star">${it.icon || '✦'}</span></div>`
).join('');
sec.innerHTML = `<div class="layers">${layers}</div><div class="bg-text"><h2>${p.title || '世界起源'}</h2>${p.body_html || ''}</div>`;
}
function renderLetter(p){
const sec = document.getElementById('letter');
if (!sec) return;
sec.innerHTML = `<div class="letter-wrap"><h2>${p.title || ''}</h2><div class="lt-sub">${p.subtitle || ''}</div><div class="lt-body">${p.body_html || ''}</div><div class="lt-sign">—— 宇森开发组</div></div>`;
}
// ---------- 规则 ----------
async function loadRules(){
const el = document.querySelector('#rules .rules-scroll');

View File

@ -52,6 +52,8 @@
.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;}
@ -201,13 +203,13 @@
<div class="logo"><span class="cn">宇森</span></div>
<nav class="nav">
<button data-tab="intro" class="active">介绍</button>
<button data-tab="features">特色</button>
<button data-tab="background">背景</button>
<button data-tab="rules">规则</button>
<button data-tab="codex">图鉴</button>
<button data-tab="forum">论坛</button>
<button data-tab="announce">公告</button>
<button data-tab="letter">致玩家</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>
@ -232,10 +234,7 @@
</div>
</section>
<!-- ===== 特色 ===== -->
<section class="panel" id="features"></section>
<!-- ===== 背景 ===== -->
<!-- ===== 背景(特色 + 世界背景 + 致玩家 合并平铺) ===== -->
<section class="panel" id="background"></section>
<!-- ===== 规则 ===== -->
@ -362,8 +361,17 @@
</div>
</section>
<!-- ===== 致玩家 ===== -->
<section class="panel" id="letter"></section>
<!-- ===== 剧情 ===== -->
<section class="panel" id="story"></section>
<!-- ===== 世界数据(登录后可见) ===== -->
<section class="panel" id="worlddata">
<div class="sec-title">世界数据<small>WORLD DATA</small></div>
<div id="worldDataContent" style="text-align:center;padding:60px 20px;color:#556677;font-size:15px;">
<div style="font-size:40px;margin-bottom:14px;">🌍</div>
加载中…
</div>
</section>
</main>
</div>
@ -587,6 +595,7 @@
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){
@ -594,6 +603,7 @@
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){}

View File

@ -3,64 +3,103 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏介绍 — 宇森</title>
<title>宇森 — 游戏百科</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html { scroll-behavior:smooth; }
body {
font-family:'Microsoft YaHei','PingFang SC',sans-serif;
background:#06090f; color:#c8d6e5;
line-height:1.7;
background:#06090f; color:#c8d6e5; line-height:1.7;
}
#particles {
position:fixed; top:0; left:0; width:100%; height:100%;
z-index:0; pointer-events:none;
}
/* ===== 顶栏导航tab 切换) ===== */
nav {
position:fixed; top:0; left:0; right:0; z-index:1000;
background:rgba(6,9,15,0.85); backdrop-filter:blur(12px);
border-bottom:1px solid rgba(100,200,255,0.1);
padding:0 40px; height:56px;
display:flex; align-items:center; justify-content:space-between;
background:rgba(6,9,15,.88); backdrop-filter:blur(14px);
border-bottom:1px solid rgba(100,200,255,.1);
padding:0 32px; height:56px;
display:flex; align-items:center; gap:6px;
}
nav .logo { font-size:18px; font-weight:bold; text-decoration:none; letter-spacing:2px; background:linear-gradient(135deg, #4af, #a8e6cf); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav .links { display:flex; gap:28px; list-style:none; }
nav .links a { color:#667788; text-decoration:none; font-size:14px; transition:color 0.3s; }
nav .links a:hover, nav .links a.active { color:#4af; }
nav .logo {
font-size:18px; font-weight:bold; letter-spacing:2px;
background:linear-gradient(135deg,#4af,#a8e6cf);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
text-decoration:none; margin-right:20px;
}
nav .tab {
color:#667788; text-decoration:none; font-size:14px;
padding:7px 16px; border-radius:8px; transition:all .2s;
cursor:pointer; border:none; background:none; font-family:inherit;
}
nav .tab:hover { color:#aaccdd; background:rgba(100,200,255,.06); }
nav .tab.active {
color:#4af; background:rgba(74,175,255,.12);
box-shadow:inset 0 0 0 1px rgba(74,175,255,.2);
}
#auth-area { margin-left:auto; margin-right:8px; }
/* ===== 主内容区 ===== */
.page {
position:relative; z-index:1; padding:90px 32px 60px; max-width:900px; margin:0 auto;
position:relative; z-index:1; padding:76px 36px 60px;
max-width:980px; margin:0 auto;
}
.tab-panel { display:none; }
.tab-panel.active { display:block; animation:fadeIn .25s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(6px);} to{opacity:1;transform:none;} }
h1 {
font-size:32px; font-weight:bold; margin-bottom:8px;
font-size:28px; font-weight:bold; margin-bottom:6px;
background:linear-gradient(135deg,#4af,#a8e6cf);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
}
.page-sub { color:#556677; font-size:15px; margin-bottom:48px; }
.page-sub { color:#556677; font-size:14px; margin-bottom:32px; }
h2 {
font-size:24px; color:#e0e0e0; margin:48px 0 16px; padding-left:12px;
font-size:22px; color:#e0e0e0; margin:36px 0 14px; padding-left:12px;
border-left:3px solid #4af;
}
p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
.features { display:grid; grid-template-columns:repeat(2, 1fr); gap:20px; margin-top:24px; }
.feature-card {
background:rgba(10,20,40,0.5); border:1px solid rgba(100,200,255,0.1);
border-radius:12px; padding:28px 22px;
transition:transform 0.3s, border-color 0.3s;
h3 {
font-size:17px; color:#d0dce8; margin:20px 0 10px;
}
.feature-card:hover { transform:translateY(-3px); border-color:rgba(100,200,255,0.3); }
.feature-card .icon { font-size:32px; margin-bottom:12px; }
.feature-card h3 { font-size:16px; color:#e0e0e0; margin-bottom:8px; }
p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }
/* ===== 背景页 · 内容章节(平铺,非子菜单) ===== */
.bg-section { margin-bottom:40px; }
.bg-section h2 { margin-top:0; }
.features { display:grid; grid-template-columns:repeat(2,1fr); gap:18px; margin-top:18px; }
.feature-card {
background:rgba(10,20,40,.5); border:1px solid rgba(100,200,255,.1);
border-radius:12px; padding:24px 20px;
transition:transform .25s,border-color .25s;
}
.feature-card:hover { transform:translateY(-3px); border-color:rgba(100,200,255,.28); }
.feature-card .icon { font-size:30px; margin-bottom:10px; }
.feature-card h3 { font-size:15px; color:#e0e0e0; margin-bottom:6px; }
.feature-card p { font-size:13px; color:#8899aa; line-height:1.6; margin:0; }
@media (max-width:768px) {
.features { grid-template-columns:1fr; }
h1 { font-size:24px; } nav .links { display:none; }
.page { padding:80px 20px 40px; }
/* ===== 其他面板通用 ===== */
.panel-placeholder {
text-align:center; padding:80px 20px; color:#556677;
font-size:15px;
}
.panel-placeholder .icon { font-size:48px; margin-bottom:16px; }
/* ===== 响应式 ===== */
@media (max-width:768px) {
nav { padding:0 16px; flex-wrap:wrap; height:auto; gap:4px; padding-top:8px;padding-bottom:8px; }
nav .tab { font-size:13px; padding:6px 12px; }
nav .logo { margin-right:12px; }
.page { padding:96px 16px 40px; }
.features { grid-template-columns:1fr; }
}
/* ===== 加载态 ===== */
.loading { text-align:center; color:#556677; padding:40px; }
</style>
<link rel="stylesheet" href="auth.css">
</head>
@ -69,22 +108,25 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
<canvas id="particles"></canvas>
<nav>
<a href="index.html" class="logo">宇森</a>
<a href="index.html" class="logo">✦ 宇森</a>
<button class="tab active" data-tab="intro">介绍</button>
<button class="tab" data-tab="bg">背景</button>
<button class="tab" data-tab="rules">规则</button>
<button class="tab" data-tab="codex">图鉴</button>
<button class="tab" data-tab="forum">论坛</button>
<button class="tab" data-tab="announce">公告</button>
<button class="tab" data-tab="story">剧情</button>
<div id="auth-area"></div>
<ul class="links">
<li><a href="intro.html" class="active">游戏介绍</a></li>
<li><a href="platform.html">平台架构</a></li>
<li><a href="world.html">世界结构</a></li>
<li><a href="races.html">种族</a></li>
<li><a href="demo.html">演示</a></li>
</ul>
</nav>
<div class="page">
<h1>游戏介绍</h1>
<p class="page-sub">这个世界不是静止的布景,它有地质、气候、生态和文明演化逻辑</p>
<div style="background:rgba(10,20,40,0.4); border-radius:12px; padding:32px 28px; margin-bottom:32px;">
<!-- ========== 介绍 ========== -->
<div class="tab-panel active" id="panel-intro">
<h1>宇 森</h1>
<p class="page-sub">寰 宇 · 森 罗 — 一个活的文明系统</p>
<div style="background:rgba(10,20,40,.4); border-radius:12px; padding:30px 26px;">
<p style="font-size:15px;color:#b0c0d0;">
你的角色不是操控者,而是<strong style="color:#4af;">「园丁」</strong>——播种、引导、在关键时刻施加影响,然后看着文明顺着自己的逻辑生长。
</p>
@ -97,7 +139,40 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
</p>
</div>
<h2>核心特色</h2>
<h2>快速了解</h2>
<div class="features">
<div class="feature-card">
<div class="icon">🧬</div>
<h3>活的文明</h3>
<p>NPC 有性格、记忆和自主决策能力,不是站桩的静态单位。</p>
</div>
<div class="feature-card">
<div class="icon">🏔️</div>
<h3>五层垂直世界</h3>
<p>地心 / 地下 / 大地 / 天空 / 星空,层间资源和事件互通。</p>
</div>
<div class="feature-card">
<div class="icon">🔭</div>
<h3>镜头语言</h3>
<p>点击格子 → 飞入微观场景,建筑动画、人物走动、资源飘字。</p>
</div>
<div class="feature-card">
<div class="icon">🌐</div>
<h3>独立世界 + 异步互动</h3>
<p>每人一个世界,通过贸易 / 战争 / 联盟产生连接。</p>
</div>
</div>
</div>
<!-- ========== 背景(特色 + 世界背景 + 致玩家 整合) ========== -->
<div class="tab-panel" id="panel-bg">
<h1>背 景</h1>
<p class="page-sub">世界观设定 · 开发理念 · 给玩家的话</p>
<!-- 子板块一:核心特色 -->
<div class="bg-section">
<h2>✦ 核心特色</h2>
<p>宇森不是传统沙盒——每个系统都围绕「活的文明」这一核心理念设计。</p>
<div class="features">
<div class="feature-card">
<div class="icon">🧬</div>
@ -107,7 +182,7 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
<div class="feature-card">
<div class="icon">🏔️</div>
<h3>五层垂直世界</h3>
<p>地心 / 地下 / 地 / 天空 / 星空,五层是同一世界的不同切面,层间有资源和事件互动。</p>
<p>地心 / 地下 / 地 / 天空 / 星空,五层是同一世界的不同切面,层间有资源和事件互动。</p>
</div>
<div class="feature-card">
<div class="icon">🔭</div>
@ -132,7 +207,77 @@ p { color:#99aabb; font-size:15px; margin-bottom:16px; line-height:1.9; }
</div>
</div>
<!-- 子板块二:世界背景 -->
<div class="bg-section">
<h2>✦ 世界背景</h2>
<p>你的角色不是操控者,而是<strong style="color:#4af;">「园丁」</strong>——播种、引导、在关键时刻施加影响,然后看着文明顺着自己的逻辑生长。</p>
<p>每个世界由五层垂直空间组成:从地心的熔岩脉搏,到地表上繁衍的城镇,再到星空中的未知信号。层与层之间不是割裂的维度,而是同一世界的不同切面——地下的矿脉供养地表的炉火,天空的季风影响作物的丰歉,星空的异常预示着文明的转折。</p>
<p>文明中的个体NPC有自己的性格、记忆和成长轨迹。他们会在火灾后学会预防在交易中积累智慧在危机中推举领袖。<br><strong style="color:#a8e6cf;">这是一个活的文明系统。</strong></p>
<h3>时间与季节</h3>
<p>世界拥有独立的昼夜循环和四季更替。时间不仅影响光照和氛围,还驱动 NPC 的作息、作物的生长、以及某些事件的发生概率。玩家可以通过时钟控件加速或暂停时间流逝。</p>
<h3>五层结构</h3>
<p><strong>星空</strong> — 未知的信号源,可能来自远古文明或外星存在。<br>
<strong>天空</strong> — 季风与气候层,影响地表农业和航行。<br>
<strong>大地</strong> — 文明的主舞台,城镇、农田、道路在此展开。<br>
<strong>地下</strong> — 矿脉与遗迹,为地表提供资源。<br>
<strong>地心</strong> — 熔岩与能量源头,极端环境下的特殊生态。</p>
</div>
<!-- 子板块三:致玩家 -->
<div class="bg-section">
<h2>✦ 致玩家</h2>
<p style="font-size:16px;color:#c8d6e5;line-height:2;">
欢迎来到宇森。这里没有既定的胜利条件,也没有强制的主线任务。<br><br>
你可以花一下午看 NPC 们如何从零建起一座小镇;也可以专注于地形塑造,把世界雕成你心中的样子;或者什么都不做,只是静静地看着昼夜交替、四季流转。<br><br>
这个世界会记住你的每一次选择。你种下的因,会在某个意想不到的时刻结出果——也许是一棵参天大树,也许是一场席卷大陆的战争,也许只是一个居民在酒馆里提起你的名字。<br><br>
<strong style="color:#a8e6cf;">去探索吧,园丁。这个世界在等你。</strong>
</p>
</div>
</div>
<!-- ========== 规则 ========== -->
<div class="tab-panel" id="panel-rules">
<h1>规 则</h1>
<p class="page-sub">游戏机制与玩法说明</p>
<div id="rules"><div class="loading">加载中...</div></div>
</div>
<!-- ========== 图鉴 ========== -->
<div class="tab-panel" id="panel-codex">
<h1>图 鉴</h1>
<p class="page-sub">世界中的万物记录</p>
<div id="codex"><div class="loading">加载中...</div></div>
</div>
<!-- ========== 论坛 ========== -->
<div class="tab-panel" id="panel-forum">
<h1>论 坛</h1>
<p class="page-sub">玩家讨论与交流</p>
<div id="forum"><div class="loading">加载中...</div></div>
</div>
<!-- ========== 公告 ========== -->
<div class="tab-panel" id="panel-announce">
<h1>公 告</h1>
<p class="page-sub">官方通知与更新日志</p>
<div id="announce"><div class="loading">加载中...</div></div>
</div>
<!-- ========== 剧情(新增) ========== -->
<div class="tab-panel" id="panel-story">
<h1>剧 情</h1>
<p class="page-sub">世界观主线与支线叙事</p>
<div class="panel-placeholder">
<div class="icon">📖</div>
<p>剧情内容正在筹备中…</p>
<p style="font-size:13px;margin-top:8px;color:#445566;">敬请期待</p>
</div>
</div>
</div><!-- /.page -->
<script>
/* ===== 粒子背景 ===== */
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
let particles = [];
@ -140,15 +285,30 @@ function resize() { canvas.width = window.innerWidth; canvas.height = window.inn
resize(); window.addEventListener('resize', resize);
class Particle {
constructor() { this.reset(); this.y = Math.random() * canvas.height; }
reset() { this.x = Math.random() * canvas.width; this.y = -10; this.size = Math.random()*1.5+0.5; this.speed = Math.random()*0.3+0.1; this.opacity = Math.random()*0.5+0.1; }
reset() { this.x = Math.random()*canvas.width; this.y=-10; this.size=Math.random()*1.5+.5; this.speed=Math.random()*.3+.1; this.opacity=Math.random()*.5+.1; }
update() { this.y+=this.speed; if(this.y>canvas.height+10)this.reset(); }
draw() { ctx.fillStyle=`rgba(100,200,255,${this.opacity})`; ctx.beginPath(); ctx.arc(this.x,this.y,this.size,0,Math.PI*2); ctx.fill(); }
}
for(let i=0;i<80;i++) particles.push(new Particle());
function animate(){ctx.clearRect(0,0,canvas.width,canvas.height);particles.forEach(p=>{p.update();p.draw();});requestAnimationFrame(animate);}
animate();
/* ===== Tab 切换 ===== */
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('.tab-panel');
function switchTab(name) {
tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === name));
panels.forEach(p => p.classList.toggle('active', p.id === 'panel-'+name));
// URL hash 不跳转,静默切换
history.replaceState(null,'','#'+name);
}
tabs.forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
// 支持 hash 直接访问
if(location.hash){const h=location.hash.slice(1);if(document.getElementById('panel-'+h))switchTab(h);}
</script>
<script src="auth.js"></script>
<script src="assets/game-api.js"></script>
<script src="assets/site-app.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>

View File

@ -3323,33 +3323,43 @@ init();
<button class="nc-se" data-cmd="se"></button>
</div>
<script>
// 右下角导航控件:方向按钮 + 键盘方向键,统一平移相机
const PAN_STEP = 120; // [PLACEHOLDER] 每按一步平移像素量
// 右下角导航控件:方向按钮(支持长按) + 键盘方向键,统一平移相机
const PAN_STEP = 20; // [PLACEHOLDER] 每步平移像素量(按钮单次 / 键盘单次)
const PAN_HOLD_MS = 55; // [PLACEHOLDER] 按钮长按重复间隔ms
const PAN_BOUND_K = 0.45; // [PLACEHOLDER] 平移上限 = 视口边长的比例(向内收缩,世界中心最多移到屏幕内约 45% 处即停)
let panTimer = null;
function stopPanHold() { if (panTimer) { clearInterval(panTimer); panTimer = null; } }
function panCamera(dx, dy) {
if (microViewOpen) return; // 微观视图打开时不平移主世界
camera.x += dx;
camera.y += dy;
const bx = W * PAN_BOUND_K, by = H * PAN_BOUND_K;
camera.x = Math.max(-bx, Math.min(bx, camera.x + dx));
camera.y = Math.max(-by, Math.min(by, camera.y + dy));
}
document.getElementById('navControls').addEventListener('click', e => {
const PAN_DIRS = {
up:[0,-PAN_STEP], down:[0,PAN_STEP], left:[-PAN_STEP,0], right:[PAN_STEP,0],
nw:[-PAN_STEP,-PAN_STEP], ne:[PAN_STEP,-PAN_STEP], sw:[-PAN_STEP,PAN_STEP], se:[PAN_STEP,PAN_STEP],
};
const navPanel = document.getElementById('navControls');
navPanel.addEventListener('pointerdown', e => {
const btn = e.target.closest('button');
if (!btn) return;
switch (btn.dataset.cmd) {
case 'up': panCamera(0, -PAN_STEP); break;
case 'down': panCamera(0, PAN_STEP); break;
case 'left': panCamera(-PAN_STEP, 0); break;
case 'right': panCamera( PAN_STEP, 0); break;
case 'nw': panCamera(-PAN_STEP, -PAN_STEP); break;
case 'ne': panCamera( PAN_STEP, -PAN_STEP); break;
case 'sw': panCamera(-PAN_STEP, PAN_STEP); break;
case 'se': panCamera( PAN_STEP, PAN_STEP); break;
}
e.preventDefault();
const d = PAN_DIRS[btn.dataset.cmd];
if (!d) return;
stopPanHold();
panCamera(d[0], d[1]); // 立即响应一次
panTimer = setInterval(() => panCamera(d[0], d[1]), PAN_HOLD_MS); // 长按连续平移
});
// 键盘方向键 = 等效于方向按钮(按住时系统自动重复 keydown → 连续平移)
window.addEventListener('pointerup', stopPanHold);
window.addEventListener('pointercancel', stopPanHold);
// 键盘方向键 + WASD = 等效于方向按钮(按住时系统自动重复 keydown → 连续平移)
const KEY_PAN = {
ArrowUp: [0, -PAN_STEP],
ArrowDown: [0, PAN_STEP],
ArrowLeft: [-PAN_STEP, 0],
ArrowRight: [ PAN_STEP, 0],
ArrowUp: [0, -PAN_STEP], ArrowDown: [0, PAN_STEP],
ArrowLeft: [-PAN_STEP, 0], ArrowRight: [ PAN_STEP, 0],
w: [0, -PAN_STEP], s: [0, PAN_STEP],
a: [-PAN_STEP, 0], d: [ PAN_STEP, 0],
W: [0, -PAN_STEP], S: [0, PAN_STEP],
A: [-PAN_STEP, 0], D: [ PAN_STEP, 0],
};
window.addEventListener('keydown', e => {
if (!(e.key in KEY_PAN)) return;