feat: 网站布局统一化+公告卡片重做+地形素材归类+加载动画升级+底部栏拆分

- 网站 7 个页签统一内边距/宽度/滚动方式,去掉重复页头
- 公告卡片改为图文大卡(支持后台 image/title/pinned 字段)
- assets/terrain/ 拆 6 个子目录并同步 manifest + TERRAIN_ASSETS
- 删除 assets/art/(transition 图片已清空)
- 加载界面增加随机过渡背景图 + 深色压暗层
- 底部栏建造按钮拆为地形/移动/高度/管理 4 个独立按钮
- 侧面板改为从底部向上展开(WorldBox 风格)
- 删除视频背景文件(已回滚为静态 JPG)
- 新增 wc1.js(六边形地图渲染)、wc2.js(导航控件)
This commit is contained in:
李鹏宇 2026-07-24 23:39:49 +08:00
parent f12cab0b53
commit e405eb7a07
60 changed files with 3423 additions and 309 deletions

View File

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
import json
import logging
import re
from odoo import http
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
def _json(data):
"""返回 UTF-8 JSON 响应(与 game_base 一致)。"""
@ -59,3 +63,26 @@ class YtWorldApi(http.Controller):
if world and settings is not None:
world.settings = settings
return _json({'ok': True})
@http.route('/yt_world/api/world/rename', type='http', auth='user',
methods=['POST', 'OPTIONS'], csrf=False)
def rename_world(self, **kw):
"""修改当前用户世界名称。body: {"name": "..."}
校验:trim 1-24 字符,禁控制字符,空白正常允许"""
user = request.env.user
world = request.env['yt.world'].sudo().search(
[('user_id', '=', user.id)], limit=1)
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
name = (body.get('name') or '').strip()
if not name:
return _json({'ok': False, 'error': 'empty'})
if len(name) > 24:
return _json({'ok': False, 'error': 'too_long'})
if re.search(r'[\x00-\x1f\x7f]', name):
return _json({'ok': False, 'error': 'invalid_char'})
old = world.name
world.name = name
_logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name)
return _json({'ok': True, 'name': name})

View File

@ -87,6 +87,13 @@ const API = {
body: JSON.stringify({ settings }),
})).json();
},
async renameWorld(name) {
return (await fetch('/yt_world/api/world/rename?db=' + WORLD_DB, {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})).json();
},
// ---------- 世界数据 / 包裹后端接口预留yt_world 暂未实现路由,前端 mock 兜底) ----------
async worldData() {
try {

View File

@ -64,9 +64,11 @@ async function loadForum(){
// 分类筛选
const cats = [];
list.forEach(t => { if (t.category && cats.indexOf(t.category) < 0) cats.push(t.category); });
const catLabels = {};
list.forEach(t => { if (t.category) catLabels[t.category] = t.category_label || t.category; });
if (catsEl){
catsEl.innerHTML = `<div class="ch active" data-fcat="all">全部</div>` +
cats.map(c => `<div class="ch" data-fcat="${c}">${c}</div>`).join('');
cats.map(c => `<div class="ch" data-fcat="${c}">${catLabels[c] || c}</div>`).join('');
catsEl.querySelectorAll('.ch').forEach(b => b.addEventListener('click', () => {
catsEl.querySelectorAll('.ch').forEach(x => x.classList.remove('active'));
b.classList.add('active');
@ -90,9 +92,22 @@ async function loadAnnounce(){
try {
const list = await API.announcements();
if (!list || !list.length){ el.innerHTML = '<div class="note"><div class="body">暂无公告</div></div>'; return; }
el.innerHTML = list.map(n =>
`<div class="note"><div class="date">${n.date}</div><div class="body">${n.body_html || ''}</div></div>`
).join('');
el.innerHTML = list.map((n, i) => {
// 封面图:后台返回 image 字段则铺满封面;否则渐变占位(图片位从后台取)
const cover = n.image
? `<div class="note-cover" style="background-image:url('${n.image}')"></div>`
: `<div class="note-cover empty"></div>`;
const title = n.title ? `<div class="note-title">${n.title}</div>` : '';
const feat = (i === 0) ? ' feat' : '';
return `<div class="note${n.pinned ? ' pin' : ''}${feat}">
${cover}
<div class="note-body">
<div class="date">${n.date || ''}</div>
${title}
<div class="body">${n.body_html || ''}</div>
</div>
</div>`;
}).join('');
} catch (e) { /* 保留静态兜底内容 */ }
}

View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 377 KiB

View File

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

View File

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 384 KiB

View File

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 605 KiB

View File

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 621 KiB

View File

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

View File

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View File

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 368 KiB

View File

Before

Width:  |  Height:  |  Size: 375 KiB

After

Width:  |  Height:  |  Size: 375 KiB

View File

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 362 KiB

View File

@ -1,60 +1,60 @@
{
"desert": [
"desert_1.png",
"desert_2.png",
"desert_3.png",
"desert_4.png",
"desert_5.png",
"desert_6.png",
"desert_7.png"
],
"snow": [
"snow_1.png",
"snow_2.png",
"snow_3.png",
"snow_4.png",
"snow_5.png",
"snow_6.png",
"snow_8.png"
"desert/desert_1.png",
"desert/desert_2.png",
"desert/desert_3.png",
"desert/desert_4.png",
"desert/desert_5.png",
"desert/desert_6.png",
"desert/desert_7.png"
],
"forest": [
"forest_1.png",
"forest_2.png",
"forest_3.png",
"forest_5.png",
"forest_6.png",
"forest_7.png",
"forest_8.png"
],
"plain": [
"plain_1.png",
"plain_2.png",
"plain_3.png",
"plain_4.png",
"plain_5.png",
"plain_6.png",
"plain_7.png",
"plain_8.png"
"forest/forest_1.png",
"forest/forest_2.png",
"forest/forest_3.png",
"forest/forest_5.png",
"forest/forest_6.png",
"forest/forest_7.png",
"forest/forest_8.png"
],
"mountain": [
"mountain_1.png",
"mountain_2.png",
"mountain_3.png",
"mountain_4.png",
"mountain_5.png",
"mountain_6.png",
"mountain_7.png",
"mountain_8.png"
"mountain/mountain_1.png",
"mountain/mountain_2.png",
"mountain/mountain_3.png",
"mountain/mountain_4.png",
"mountain/mountain_5.png",
"mountain/mountain_6.png",
"mountain/mountain_7.png",
"mountain/mountain_8.png"
],
"plain": [
"plain/plain_1.png",
"plain/plain_2.png",
"plain/plain_3.png",
"plain/plain_4.png",
"plain/plain_5.png",
"plain/plain_6.png",
"plain/plain_7.png",
"plain/plain_8.png"
],
"snow": [
"snow/snow_1.png",
"snow/snow_2.png",
"snow/snow_3.png",
"snow/snow_4.png",
"snow/snow_5.png",
"snow/snow_6.png",
"snow/snow_8.png"
],
"water": [
"water_1.png",
"water_2.png",
"water_3.png",
"water_4.png",
"water_5.png",
"water_6.png",
"water_7.png",
"water_8.png",
"water_9.png"
"water/water_1.png",
"water/water_2.png",
"water/water_3.png",
"water/water_4.png",
"water/water_5.png",
"water/water_6.png",
"water/water_7.png",
"water/water_8.png",
"water/water_9.png"
]
}

View File

Before

Width:  |  Height:  |  Size: 611 KiB

After

Width:  |  Height:  |  Size: 611 KiB

View File

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

Before

Width:  |  Height:  |  Size: 504 KiB

After

Width:  |  Height:  |  Size: 504 KiB

View File

Before

Width:  |  Height:  |  Size: 633 KiB

After

Width:  |  Height:  |  Size: 633 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 465 KiB

After

Width:  |  Height:  |  Size: 465 KiB

View File

Before

Width:  |  Height:  |  Size: 430 KiB

After

Width:  |  Height:  |  Size: 430 KiB

View File

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 482 KiB

View File

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 544 KiB

View File

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 526 KiB

View File

Before

Width:  |  Height:  |  Size: 530 KiB

After

Width:  |  Height:  |  Size: 530 KiB

View File

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 514 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View File

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 404 KiB

View File

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 415 KiB

View File

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

View File

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 512 KiB

View File

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 463 KiB

View File

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 386 KiB

View File

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 372 KiB

View File

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

View File

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 405 KiB

View File

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 390 KiB

View File

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

View File

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

View File

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -57,8 +57,16 @@
.cta:hover{filter:brightness(1.08);}
.content{flex:1;position:relative;overflow:hidden;}
.panel{position:absolute;inset:0;display:none;flex-direction:column;padding:30px 44px;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;}
@ -77,8 +85,6 @@
/* ===== 特色 ===== */
#features{align-items:center;justify-content:center;gap:22px;}
.sec-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:32px;letter-spacing:4px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.5);}
.sec-title small{display:block;font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;margin-top:7px;}
.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);}
@ -87,8 +93,8 @@
.card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
/* ===== 背景 ===== */
#background{flex-direction:column;align-items:stretch;gap:0;overflow-y:auto;padding:48px 6% 90px;}
.bg-page{max-width:1060px;width:100%;margin:0 auto;padding:30px 0;border-top:1px solid var(--card-bd);}
#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;
@ -120,7 +126,7 @@
.bg-text p{font-size:14.5px;color:var(--text-dim);line-height:1.95;max-width:640px;}
/* ===== 规则 ===== */
#rules{flex-direction:column;gap:14px;}
#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;}
@ -132,8 +138,8 @@
.tier-row .tdesc{font-size:13px;color:var(--text-dim);line-height:1.5;}
/* ===== 图鉴 ===== */
#codex{padding:22px 44px;gap:0;}
.codex-wrap{display:flex;flex-direction:column;gap:13px;height:100%;min-height:0;}
#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;}
@ -146,7 +152,7 @@
.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(176px,1fr));gap:14px;overflow-y:auto;padding:4px 6px 4px 0;align-content:start;flex:1;min-height:0;}
.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));}
@ -159,38 +165,49 @@
.cx .ds{font-size:11.5px;color:var(--text-dim);line-height:1.5;margin-top:6px;}
/* ===== 论坛 ===== */
#forum{flex-direction:row;gap:22px;}
.f-cats{flex:0 0 180px;display:flex;flex-direction:column;gap:8px;}
.f-cats .ch{font-size:14px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:11px 14px;border-radius:10px;cursor:pointer;transition:.2s;}
.f-cats .ch:hover{color:#fff;}
.f-cats .ch.active{color:#fff;background:rgba(139,92,246,.22);border-color:var(--magenta);}
#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;}
.f-head{display:flex;align-items:center;margin-bottom:12px;gap:12px;}
.f-head h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:24px;letter-spacing:2px;color:#fff;}
.f-head .new{margin-left:auto;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:8px 15px;border-radius:9px;font-weight:700;cursor:pointer;}
.f-head .new:hover{filter:brightness(1.08);}
.threads{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px;}
.th{display:flex;align-items:center;gap:14px;background:var(--card);border:1px solid var(--card-bd);border-radius:11px;padding:13px 16px;transition:.2s;cursor:pointer;}
.th:hover{border-color:var(--violet);background:rgba(139,92,246,.10);}
.th .pin{font-size:11px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.14);border:1px solid rgba(231,184,92,.42);border-radius:5px;padding:1px 7px;}
.th .tt{flex:1;font-size:15px;color:#fff;}
.th .meta{font-size:12px;color:var(--text-dim);white-space:nowrap;}
.th .rp{color:var(--cyan);font-weight:500;}
.post-form{display:none;flex-direction:column;gap:8px;background:var(--card);border:1px solid var(--magenta);border-radius:12px;padding:14px;margin-bottom:12px;}
.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:8px;color:var(--text);padding:9px 12px;font-family:inherit;font-size:13px;outline:none;}
.post-form textarea{resize:vertical;min-height:64px;}
.post-form .row{display:flex;gap:8px;justify-content:flex-end;}
.post-form .row button{font-size:13px;padding:7px 16px;border-radius:8px;cursor:pointer;border:1px solid var(--card-bd);background:var(--card);color:var(--text);}
.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:18px;}
#announce .notes{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;overflow-y:auto;padding-right:6px;}
.note{border-left:3px solid var(--magenta);padding:12px 16px;background:var(--card);border:1px solid var(--card-bd);border-left:3px solid var(--magenta);border-radius:0 12px 12px 0;}
.note .date{font-family:"Press Start 2P",monospace;font-size:9px;color:var(--cyan);margin-bottom:8px;}
.note .body{font-size:14px;color:var(--text);line-height:1.7;}
.note.pin{box-shadow:var(--glow);}
#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;}
@ -204,19 +221,18 @@
#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:16px;}
.f-cats{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;gap:8px;}
#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{max-width:900px;width:100%;}
.threads{width:100%;}
/* ===== 公告(居左竖排) ===== */
#announce{align-items:stretch;}
#announce .notes{max-width:1080px;width:100%;margin:0;}
/* ===== 公告(内容居中) ===== */
#announce .notes{margin:0;}
/* ===== 剧情(小说阅读器:左目录 + 右正文,竖排阅读感) ===== */
#story{padding:30px 44px;}
.story-wrap{display:flex;flex-direction:row;width:100%;height:100%;min-height: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;}
@ -230,28 +246,28 @@
.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:760px;margin:0 auto;padding:44px 48px 90px;}
.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:30px;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:15.5px;color:var(--text);line-height:2.15;}
.reader-inner .body p{margin-bottom:18px;text-indent:2em;}
.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-y:auto;padding:46px 6% 90px;}
#worldDataContent{max-width:1080px;width:100%;margin:0 auto;}
#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(190px,1fr));gap:16px;}
.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:18px 20px;transition:.26s;}
.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:34px;line-height:1.1;color:#fff;margin:8px 0 2px;}
.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;}
@ -320,7 +336,6 @@
.hero .enter-wrap{bottom:28px;}
.hero .enter-btn{font-size:15px;padding:11px 30px;}
.panel{padding:18px 18px;}
.sec-title{font-size:26px;letter-spacing:2px;}
.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;}
@ -333,6 +348,9 @@
.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;}
@ -390,11 +408,12 @@
</section>
<!-- ===== 背景(特色 + 世界背景 + 致玩家 合并平铺) ===== -->
<section class="panel" id="background"></section>
<section class="panel" id="background">
<div class="panel-scroll" id="bgBody"></div>
</section>
<!-- ===== 规则 ===== -->
<section class="panel" id="rules">
<div class="sec-title">等级制度<small>TIERS &amp; RANK SYSTEM</small></div>
<div class="rules-scroll">
<!-- ① 世界等级 · 五阶段 -->
<div class="rule-block">
@ -479,15 +498,12 @@
<div class="f-cats">
<div class="ch active" data-fcat="all">全部</div>
<div class="ch" data-fcat="discuss">综合讨论</div>
<div class="ch" data-fcat="guide">玩法攻略</div>
<div class="ch" data-fcat="fan">同人创作</div>
<div class="ch" data-fcat="bug">BUG 反馈</div>
<div class="ch" data-fcat="strategy">攻略</div>
<div class="ch" data-fcat="lore">背景考据</div>
<div class="ch" data-fcat="showcase">作品秀</div>
<div class="ch" data-fcat="bug">反馈</div>
</div>
<div class="f-main">
<div class="f-head">
<h2>社区论坛</h2>
<div class="new" id="newPost">发新帖</div>
</div>
<div class="post-form" id="postForm">
<input id="pfTitle" placeholder="标题" />
<textarea id="pfBody" placeholder="说点什么…(演示:仅本次会话内可见)"></textarea>
@ -495,24 +511,96 @@
</div>
<div class="threads" id="threads">
<div class="th" data-fcat="discuss"><span class="pin">置顶</span><span class="tt">【公告】宇森官网第一阶段上线,欢迎园丁们入驻</span><span class="meta"><span class="rp">128</span> 回复 · 官方</span></div>
<div class="th" data-fcat="guide"><span class="tt">新手向:园丁视角的三种开局思路</span><span class="meta"><span class="rp">42</span> 回复 · 行者</span></div>
<div class="th" data-fcat="strategy"><span class="tt">新手向:园丁视角的三种开局思路</span><span class="meta"><span class="rp">42</span> 回复 · 行者</span></div>
<div class="th" data-fcat="discuss"><span class="tt">你们更喜欢哪种族?来投个票</span><span class="meta"><span class="rp">76</span> 回复 · 雾岛</span></div>
<div class="th" data-fcat="fan"><span class="tt">手绘了我的主世界浮空岛,求轻喷</span><span class="meta"><span class="rp">31</span> 回复 · 青柠</span></div>
<div class="th" data-fcat="showcase"><span class="tt">手绘了我的主世界浮空岛,求轻喷</span><span class="meta"><span class="rp">31</span> 回复 · 青柠</span></div>
<div class="th" data-fcat="bug"><span class="tt">反馈:飞入微观时偶现卡顿</span><span class="meta"><span class="rp">9</span> 回复 · K</span></div>
<div class="th" data-fcat="guide"><span class="tt">地下矿脉怎么规划最省心?</span><span class="meta"><span class="rp">18</span> 回复 · 老周</span></div>
<div class="th" data-fcat="strategy"><span class="tt">地下矿脉怎么规划最省心?</span><span class="meta"><span class="rp">18</span> 回复 · 老周</span></div>
</div>
</div>
</section>
<!-- ===== 公告 ===== -->
<section class="panel" id="announce">
<div class="sec-title">公告<small>ANNOUNCEMENTS</small></div>
<div class="notes">
<div class="note pin"><div class="date">2026.07.09</div><div class="body">宇森官网第一阶段上线:介绍 / 特色 / 背景 / 规则 / 图鉴 / 论坛 / 公告 / 致玩家 八块,单页切换呈现,介绍页即概念宣传片。</div></div>
<div class="note"><div class="date">2026.07.09</div><div class="body">图鉴系统升级:种族 204 条、系别/性格/天赋/神器/传奇事件全面扩量,装备槽位对齐 头/手/胸/裤/鞋/戒指(双)/项链/宝物;全部缩略图改为程序化矢量插画,零 AI 生图。</div></div>
<div class="note"><div class="date">2026.07.08</div><div class="body">概念宣传片《垂直生长》完成,呈现五层世界自上而下生长的镜头语言(天空层改为云朵地基块,星空层改为陨石科技带)。</div></div>
<div class="note"><div class="date">2026.07.01</div><div class="body">园丁视角核心玩法原型启动,首版六边形地图与活 NPC 验证中。</div></div>
<div class="note"><div class="date">2026.06.20</div><div class="body">图鉴系统规划确定:种族 / 建筑 / 生物 / 神器 四大类目,美术持续补充中。</div></div>
<div class="note pin feat">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.09 · 官方</div>
<div class="note-title">宇森官网第一阶段上线,欢迎园丁们入驻</div>
<div class="body">
<p>历经三个月打磨,宇森官网第一阶段正式上线。本次更新带来八大板块——介绍、特色、背景、规则、图鉴、论坛、公告、致玩家,全部以单页切换形式呈现,介绍页即一段概念宣传片,让你在三秒内读懂我们要做什么。</p>
<p>「园丁视角」是宇森的核心:你不是旁观者,而是世界的第一位居民。上传你的世界种子,观看五层世界自上而下生长——从星空陨石带到地心熔岩,每一寸地形、每一个种族,都由你的选择塑造。</p>
<p>我们同步开放了社区论坛与图鉴共建入口。欢迎在「论坛」留下你的构想,或在「图鉴」认领尚未完成的种族条目;被采纳的内容,会带上你的署名进入正式世界观。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.09</div>
<div class="note-title">图鉴系统全面升级204 种族 + 程序化矢量插画</div>
<div class="body">
<p>图鉴系统完成一次大版本升级:种族条目扩充至 204 条,系别、性格、天赋、神器与传奇事件全面扩量,装备槽位对齐「头 / 手 / 胸 / 裤 / 鞋 / 戒指(双)/ 项链 / 宝物」九宫格。</p>
<p>所有缩略图改用程序化矢量插画生成,零 AI 生图、零版权风险,且在任意分辨率下都不糊。后续会逐步开放玩家投稿的「同人词条」分区。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.08</div>
<div class="note-title">概念宣传片《垂直生长》完成</div>
<div class="body">
<p>概念宣传片《垂直生长》正式杀青。片长 90 秒,以「自上而下」的镜头语言呈现五层世界的生长过程:天空层改为云朵地基块,星空层改为陨石科技带,地心则翻涌着熔岩血脉。</p>
<p>这支片子将成为官网介绍页的自动背景,也会在后续对外宣发中使用。如果你喜欢这种克制的科幻基调,记得在论坛给我们反馈。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.07.01</div>
<div class="note-title">园丁视角核心玩法原型启动</div>
<div class="body">
<p>园丁视角核心玩法原型进入验证阶段。首版采用六边形地图,配合「活 NPC」系统——NPC 拥有独立的日程、记忆与情绪,会对你的长期行为产生不同反应。</p>
<p>这一阶段我们重点关注「放手感」:玩家拖拽、缩放、种植、引导文明,每一步都要足够顺滑。手感达标后,再叠加叙事与冲突。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.25</div>
<div class="note-title">五大世界层设定公开</div>
<div class="body">
<p>我们公开了宇森的底层世界观——五大世界层:星空、天空、大地、地下、地心。每一层对应一种文明形态与资源循环,层与层之间通过「根系」与「星轨」双向连通。</p>
<p>这套设定不是装饰,而是玩法骨架:你在大地的每一次开采,都会改变地心的压力;地心的每一次喷发,又会重塑天空的气候。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.20</div>
<div class="note-title">社区共建计划:你的名字可以写进世界观</div>
<div class="body">
<p>图鉴系统规划正式确定,分为「种族 / 建筑 / 生物 / 神器」四大类目。我们决定把相当一部分条目开放给社区共建。</p>
<p>被采纳的投稿会标注作者署名,并永久保留在官方图鉴中。第一批评审将在论坛「共建」分区进行,欢迎有想法的园丁报名。</p>
</div>
</div>
</div>
<div class="note">
<div class="note-cover empty"></div>
<div class="note-body">
<div class="date">2026.06.12</div>
<div class="note-title">限量内测招募开启</div>
<div class="body">
<p>宇森首轮封闭内测招募正式开启,名额 500 人。我们将优先邀请在论坛持续输出高质量内容的园丁,以及愿意参与玩法共创的核心用户。</p>
<p>内测的重点不是「好不好玩」,而是「哪里别扭」——你的每一条吐槽,都会直接进入我们的周度复盘会。</p>
</div>
</div>
</div>
</div>
</section>
@ -531,7 +619,6 @@
<!-- ===== 世界数据(登录后可见) ===== -->
<section class="panel" id="worlddata">
<div class="sec-title">世界数据<small>WORLD DATA</small></div>
<div id="worldDataContent"></div>
</section>
</main>
@ -737,7 +824,8 @@
// forum post (in-session demo)
const postForm=document.getElementById('postForm');
document.getElementById('newPost').addEventListener('click',()=>postForm.classList.toggle('show'));
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();
@ -791,7 +879,7 @@
// ============ 背景页:从 odoo 拉取页面板块渲染 ============
async function renderBackground(){
const box = document.getElementById('background');
const box = document.getElementById('bgBody');
if(!box) return;
try{
const r = await fetch('/game/api/pages?db=game', {credentials:'same-origin'});

2963
wc1.js Normal file

File diff suppressed because it is too large Load Diff

47
wc2.js Normal file
View File

@ -0,0 +1,47 @@
<script>
// 右下角导航控件:方向按钮(支持长按) + 键盘方向键,统一平移相机
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; // 微观视图打开时不平移主世界
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));
}
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;
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); // 长按连续平移
});
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],
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;
if (microViewOpen) return; // 微观视图不平移主世界
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
e.preventDefault(); // 阻止方向键滚动页面
panCamera(KEY_PAN[e.key][0], KEY_PAN[e.key][1]);
});

View File

@ -225,7 +225,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
/* 背景插画层:铺满 + 放大遮水印,置于内容之下 */
#flyInOverlay .flyBg {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: cover; transform: scale(1.08);
object-fit: cover; transform: scale(1.2);
z-index: 0;
}
/* 深色压暗层,保证前景文字/进度条可读 */
@ -257,10 +257,21 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
/* 加载界面 */
#loadingScreen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #0a0e17; z-index: 500;
background: transparent; overflow: hidden; z-index: 500;
display: flex; flex-direction: column; justify-content: center; align-items: center;
padding: 0 24px 56px 24px;
}
/* 随机过渡图作背景,放大遮挡水印,置于内容之下 */
#loadingScreen .loadBg {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: cover; transform: scale(1.2); z-index: 0;
}
/* 深色压暗层,保证前景文字/进度条可读 */
#loadingScreen::before {
content: ''; position: absolute; inset: 0; z-index: 1;
background: linear-gradient(180deg, rgba(10,14,23,.55) 0%, rgba(10,14,23,.82) 100%);
}
#loadingScreen > *:not(.loadBg) { position: relative; z-index: 2; }
#loadingScreen .loadLine {
display: flex; align-items: center; justify-content: flex-start; gap: 4px;
margin-top: auto; margin-bottom: 8px;
@ -448,6 +459,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
<!-- 加载界面 -->
<div id="loadingScreen">
<img class="loadBg" id="loadBg" alt="">
<div class="loadTip" id="loadTip"></div>
<div class="loadLine">
<div class="loadBar"><div class="loadFill" id="loadFill"></div></div>
@ -483,7 +495,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
<div class="bb-slots">
<button class="bb-btn" id="btnData" type="button">📊 数据</button>
<button class="bb-btn" id="btnInventory" type="button">🎒 包裹</button>
<button class="bb-btn" id="btnBuild" type="button">🔨 建造</button>
<button class="bb-btn" id="btnTerrain" type="button">🌿 地形</button>
<button class="bb-btn" id="btnMove" type="button">✋ 移动</button>
<button class="bb-btn" id="btnHeight" type="button">⛰️ 高度</button>
<button class="bb-btn" id="btnManage" type="button">⚙️ 管理</button>
<button class="bb-btn" id="btnTech" type="button">🔬 科技</button>
</div>
</div>
@ -629,21 +644,13 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
box-shadow:8px 0 40px rgba(0,0,0,.45);
}
#sidePanel.open{transform:translateX(0);}
#spTabs{display:flex;gap:0;flex-shrink:0;border-bottom:1px solid rgba(100,200,255,.15);
background:linear-gradient(180deg,rgba(12,18,32,.98),rgba(8,12,22,.95));}
.sp-tab{flex:1;padding:12px 6px;background:none;border:none;color:#8899aa;font-size:14px;font-weight:600;
cursor:pointer;transition:.18s;border-bottom:2px solid transparent;text-align:center;}
.sp-tab:hover{color:#cfe6ff;background:rgba(100,200,255,.06);}
.sp-tab.active{color:#fff;border-bottom-color:#8b5cf6;background:rgba(139,92,246,.10);}
.sp-tab .sp-ico{display:block;font-size:18px;margin-bottom:3px;}
.sp-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:14px 16px;display:none;min-height:0;}
.sp-body.active{display:block;}
.sp-body::-webkit-scrollbar{width:4px;}
.sp-body::-webkit-scrollbar-thumb{background:rgba(100,200,255,.28);border-radius:2px;}
/* 沙盘左移(给侧面板让路) */
#gameCanvas{transition:transform .28s ease,left .28s ease,width .28s ease;}
body.side-open #gameCanvas{transform:translateX(380px);}
/* 侧栏展开时浮于主世界之上,主画布保持原位不平移(不再挤压) */
#gameCanvas{transition:transform .28s ease;}
/* ----- 包裹7列 × 行(物资分类 + 小格子 + 滚动条) ----- */
.inv-cat{font-size:11px;color:#8b5cf6;letter-spacing:1.5px;text-transform:uppercase;margin:14px 0 6px;
@ -766,14 +773,8 @@ body.side-open #gameCanvas{transform:translateX(380px);}
box-shadow:0 0 10px rgba(16,185,129,.35);}
.tech-note{font-size:11px;color:#8aa0b8;margin-top:8px;line-height:1.5;}
</style>
<!-- ===== 统一左侧面板(数据 / 包裹 / 建造 ===== -->
<!-- ===== 统一左侧面板(数据 / 包裹 / 建造 / 科技,无顶部标签栏 ===== -->
<div id="sidePanel">
<div id="spTabs">
<button class="sp-tab" data-sp="data"><span class="sp-ico">📊</span>数据</button>
<button class="sp-tab" data-sp="inventory"><span class="sp-ico">🎒</span>包裹</button>
<button class="sp-tab" data-sp="build"><span class="sp-ico">🔨</span>建造</button>
<button class="sp-tab" data-sp="tech"><span class="sp-ico">🔬</span>科技</button>
</div>
<!-- 数据 Tab -->
<div class="sp-body" id="spDataBody"></div>
<!-- 包裹 Tab7×50 格 + 物资分类) -->
@ -788,13 +789,6 @@ body.side-open #gameCanvas{transform:translateX(380px);}
<!-- 建造 Tab4 子Tab移动/地形/高度/管理) -->
<div class="sp-body" id="spBuildBody">
<div id="buildPanel">
<div class="bp-head"><span>🔨 建造工具</span></div>
<div class="bp-tools">
<button class="bp-tool active" data-tool="terrain">地形</button>
<button class="bp-tool" data-tool="move">移动</button>
<button class="bp-tool" data-tool="height">高度</button>
<button class="bp-tool" data-tool="manage">管理</button>
</div>
<div class="bp-brush" id="bpBrush"></div>
<div class="bp-hctl" id="bpHeightControl" style="display:none;">
<div class="bp-hctl-row">
@ -1974,7 +1968,6 @@ const TRANSITION_IMAGES = [
'assets/art/transitions/transition_adventure.png',
'assets/art/transitions/transition_sky.png',
'assets/art/transitions/transition_forest.png',
'assets/art/transitions/transition_team.png',
'assets/art/transitions/transition_night.png',
'assets/art/transitions/transition_dawn.png',
'assets/art/transitions/transition_coast.png',
@ -2004,7 +1997,7 @@ function enterMicroWorld(tile) {
if (flyFill) flyFill.style.width = '0%';
if (flyPct) flyPct.textContent = '0%';
// 进度条动画:与最短停留时长保持同步,给玩家明确的加载反馈
const MICRO_ENTER_HOLD = 1100; // [PLACEHOLDER] 体感测试:最短 1.1s,快机可调到 900
const MICRO_ENTER_HOLD = 1500; // [PLACEHOLDER] 体感测试:最短 1.5s,快机可调到 1100
const startT = performance.now();
let rafId;
function tick(now) {
@ -3025,153 +3018,6 @@ function swapTiles(a, b) {
swapFlashes.push({ a: { q: a.q, r: a.r }, b: { q: b.q, r: b.r }, life: 1 });
}
// 建造模式 UI 绑定
function setupBuildUI() {
const toggle = document.getElementById('btnBuild');
const panel = document.getElementById('buildPanel');
const bpClose = document.getElementById('bpClose');
const bpBrush = document.getElementById('bpBrush');
const bpVariants = document.getElementById('bpVariants');
const bpHint = document.getElementById('bpHint');
const bpHeightControl = document.getElementById('bpHeightControl');
const bpHeightSlider = document.getElementById('bpHeightSlider');
const bpHeightVal = document.getElementById('bpHeightVal');
if (!toggle || !panel) return;
const bpMgmt = document.getElementById('bpMgmt');
const renderBrush = () => {
bpBrush.innerHTML = '';
bpBrush.style.display = (buildTool === 'terrain') ? 'flex' : 'none';
bpHeightControl.style.display = 'none';
bpMgmt.style.display = (buildTool === 'manage') ? 'flex' : 'none';
if (buildTool === 'terrain') {
const BRUSH_ORDER = ['water', 'desert', 'plain', 'forest', 'mountain', 'snow'];
BRUSH_ORDER.forEach(id => {
const t = TERRAIN[id];
const b = document.createElement('button');
b.className = 'bp-chip' + (buildBrush === id ? ' active' : '');
b.style.background = t.color; b.textContent = t.name; b.title = t.name;
b.onclick = () => { buildBrush = id; buildVariant = 'random'; renderBrush(); };
bpBrush.appendChild(b);
});
bpHint.textContent = '先选地形,点地块应用(素材按坐标自动选)。';
} else if (buildTool === 'height') {
bpHeightControl.style.display = 'block';
bpHeightSlider.value = String(heightTarget);
bpHeightVal.textContent = heightLabel(heightTarget);
bpHint.textContent = '滑块设定高度:左键升高 / 右键降低 / 滚轮微调。每格独立,互不影响。';
} else if (buildTool === 'manage') {
// 管理:内嵌到 buildPanel 内(地表比例滑杆 + 预设 + 应用)
bpHint.textContent = '调整地表比例,调好点应用即可重新生成世界。';
} else {
bpHint.textContent = '移动工具:点第一块再点第二块交换,或按住一块拖到另一块松手交换。';
}
};
document.querySelectorAll('.bp-tool').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.bp-tool').forEach(x => x.classList.remove('active'));
btn.classList.add('active');
buildTool = btn.dataset.tool;
renderBrush();
};
});
// 高度滑块:拖动即把当前悬停格设为滑块绝对高度(每格独立,不联动周边)
const onHeightSlide = () => {
heightTarget = parseInt(bpHeightSlider.value, 10);
bpHeightVal.textContent = heightLabel(heightTarget);
if (buildMode && buildTool === 'height' && currentLayer === 'earth' && hoveredTile) {
setHeight(hoveredTile, heightTarget, true); // 滑块拖动改高度→随机一张贴图
markDirty(hoveredTile.q, hoveredTile.r);
}
};
bpHeightSlider.addEventListener('input', onHeightSlide);
toggle.onclick = () => {
buildMode = !buildMode;
toggle.classList.toggle('active', buildMode);
panel.classList.toggle('hidden', !buildMode);
if (buildMode && currentLayer !== 'earth') switchWorld('earth');
if (!buildMode) { swapFirst = null; draggingSwap = false; dragSwapFrom = null; }
renderBrush();
};
bpClose.onclick = () => {
buildMode = false; toggle.classList.remove('active'); panel.classList.remove('active');
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
closeSidePanel();
};
// ===== 底部三按钮 → 统一左侧面板(数据/包裹/建造 Tab 切换) =====
const btnData = document.getElementById('btnData');
const btnInventory = document.getElementById('btnInventory');
const btnBuild = document.getElementById('btnBuild');
const btnTech = document.getElementById('btnTech');
const sidePanel = document.getElementById('sidePanel');
const spTabs = document.querySelectorAll('#spTabs .sp-tab');
const spBodies = document.querySelectorAll('.sp-body');
let currentSpTab = null; // 'data' | 'inventory' | 'build' | 'tech'
const SP_BODY = { data:'spDataBody', inventory:'spInvBody', build:'spBuildBody', tech:'spTechBody' };
function openSidePanel(tab) {
sidePanel.classList.add('open');
document.body.classList.add('side-open');
// switch tab
spTabs.forEach(t => t.classList.toggle('active', t.dataset.sp === tab));
spBodies.forEach(b => b.classList.toggle('active', b.id === SP_BODY[tab]));
// button states
btnData.classList.toggle('active', tab === 'data');
btnInventory.classList.toggle('active', tab === 'inventory');
btnBuild.classList.toggle('active', tab === 'build');
btnTech.classList.toggle('active', tab === 'tech');
currentSpTab = tab;
// render content on first open
if (tab === 'data') renderData();
if (tab === 'inventory') renderInventory();
if (tab === 'build') { buildMode = true; document.getElementById('buildPanel').classList.add('active'); }
if (tab === 'tech') renderTech();
}
function closeSidePanel() {
sidePanel.classList.remove('open');
document.body.classList.remove('side-open');
btnData.classList.remove('active'); btnInventory.classList.remove('active'); btnBuild.classList.remove('active'); btnTech.classList.remove('active');
// exit build mode if was active
if (currentSpTab === 'build') {
buildMode = false; toggle.classList.remove('active');
document.getElementById('buildPanel').classList.remove('active');
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
}
currentSpTab = null;
}
btnData.addEventListener('click', () => {
if (currentSpTab === 'data') { closeSidePanel(); return; }
openSidePanel('data');
});
btnInventory.addEventListener('click', () => {
if (currentSpTab === 'inventory') { closeSidePanel(); return; }
openSidePanel('inventory');
});
btnBuild.addEventListener('click', () => {
if (currentSpTab === 'build') { closeSidePanel(); return; }
openSidePanel('build');
});
btnTech.addEventListener('click', () => {
if (currentSpTab === 'tech') { closeSidePanel(); return; }
openSidePanel('tech');
});
// tab clicks inside side panel
spTabs.forEach(t => t.addEventListener('click', () => openSidePanel(t.dataset.sp)));
// buildPanel close → also close side panel
bpClose.onclick = () => {
buildMode = false; toggle.classList.remove('active');
document.getElementById('buildPanel').classList.remove('active');
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
closeSidePanel();
};
// ===== 世界数据面板(演示 + 预留 /yt_world/api/data =====
function computeWorldStats() {
const byTerrain = {};
@ -3361,9 +3207,60 @@ function setupBuildUI() {
};
}
// 平铺把所有地块高度设为最低1一键压平沿用 setHeight 派生 terrain/tier/variant
// ===== 建造模式 UI 绑定 =====
function setupBuildUI() {
const panel = document.getElementById('buildPanel');
if (!panel) return;
const bpBrush = document.getElementById('bpBrush');
const bpHint = document.getElementById('bpHint');
const bpHeightControl = document.getElementById('bpHeightControl');
const bpHeightSlider = document.getElementById('bpHeightSlider');
const bpHeightVal = document.getElementById('bpHeightVal');
const bpMgmt = document.getElementById('bpMgmt');
const renderBrush = () => {
bpBrush.innerHTML = '';
bpBrush.style.display = (buildTool === 'terrain') ? 'flex' : 'none';
bpHeightControl.style.display = 'none';
bpMgmt.style.display = (buildTool === 'manage') ? 'flex' : 'none';
if (buildTool === 'terrain') {
const BRUSH_ORDER = ['water', 'desert', 'plain', 'forest', 'mountain', 'snow'];
BRUSH_ORDER.forEach(id => {
const t = TERRAIN[id];
const b = document.createElement('button');
b.className = 'bp-chip' + (buildBrush === id ? ' active' : '');
b.style.background = t.color; b.textContent = t.name; b.title = t.name;
b.onclick = () => { buildBrush = id; buildVariant = 'random'; renderBrush(); };
bpBrush.appendChild(b);
});
bpHint.textContent = '先选地形,点地块应用(素材按坐标自动选)。';
} else if (buildTool === 'height') {
bpHeightControl.style.display = 'block';
bpHeightSlider.value = String(heightTarget);
bpHeightVal.textContent = heightLabel(heightTarget);
bpHint.textContent = '滑块设定高度:左键升高 / 右键降低 / 滚轮微调。每格独立,互不影响。';
} else if (buildTool === 'manage') {
bpHint.textContent = '调整地表比例,调好点应用即可重新生成世界。';
} else {
bpHint.textContent = '移动工具:点第一块再点第二块交换,或按住一块拖到另一块松手交换。';
}
};
// 高度滑块:拖动即把当前悬停格设为滑块绝对高度(每格独立,不联动周边)
const onHeightSlide = () => {
heightTarget = parseInt(bpHeightSlider.value, 10);
bpHeightVal.textContent = heightLabel(heightTarget);
if (buildMode && buildTool === 'height' && currentLayer === 'earth' && hoveredTile) {
setHeight(hoveredTile, heightTarget, true);
markDirty(hoveredTile.q, hoveredTile.r);
}
};
bpHeightSlider.addEventListener('input', onHeightSlide);
// 平铺按钮
const bpFlatten = document.getElementById('bpFlatten');
bpFlatten.onclick = () => {
if (bpFlatten) bpFlatten.onclick = () => {
if (!confirm('平铺把所有地块高度设为最低1当前起伏将丢失。')) return;
for (const t of tiles) { setHeight(t, MIN_HEIGHT); markDirty(t.q, t.r); }
heightTarget = MIN_HEIGHT;
@ -3371,6 +3268,74 @@ function setupBuildUI() {
bpHeightVal.textContent = heightLabel(MIN_HEIGHT);
};
// ===== 底部 7 按钮 → 统一左侧浮层面板(无顶部标签栏,切换时关闭上一个) =====
const BTN_TO_TAB = {
btnData: 'data',
btnInventory: 'inventory',
btnTerrain: 'terrain',
btnMove: 'move',
btnHeight: 'height',
btnManage: 'manage',
btnTech: 'tech',
};
const TAB_TO_BODY = {
data: 'spDataBody',
inventory: 'spInvBody',
terrain: 'spBuildBody',
move: 'spBuildBody',
height: 'spBuildBody',
manage: 'spBuildBody',
tech: 'spTechBody',
};
const BUILD_TABS = new Set(['terrain', 'move', 'height', 'manage']);
const sidePanel = document.getElementById('sidePanel');
const spBodies = document.querySelectorAll('.sp-body');
let currentSpTab = null;
function openSidePanel(tab) {
sidePanel.classList.add('open');
for (const [id, t] of Object.entries(BTN_TO_TAB)) {
const b = document.getElementById(id);
if (b) b.classList.toggle('active', t === tab);
}
const bodyId = TAB_TO_BODY[tab];
spBodies.forEach(b => b.classList.toggle('active', b.id === bodyId));
currentSpTab = tab;
if (tab === 'data') renderData();
if (tab === 'inventory') renderInventory();
if (BUILD_TABS.has(tab)) {
buildMode = true;
buildTool = tab;
document.getElementById('buildPanel').classList.add('active');
if (currentLayer !== 'earth') switchWorld('earth');
renderBrush();
}
if (tab === 'tech') renderTech();
}
function closeSidePanel() {
sidePanel.classList.remove('open');
for (const id of Object.keys(BTN_TO_TAB)) {
const b = document.getElementById(id);
if (b) b.classList.remove('active');
}
if (currentSpTab && BUILD_TABS.has(currentSpTab)) {
buildMode = false;
document.getElementById('buildPanel').classList.remove('active');
swapFirst = null; draggingSwap = false; dragSwapFrom = null;
}
currentSpTab = null;
}
for (const [id, tab] of Object.entries(BTN_TO_TAB)) {
const b = document.getElementById(id);
if (b) b.addEventListener('click', () => {
if (currentSpTab === tab) { closeSidePanel(); return; }
openSidePanel(tab);
});
}
renderBrush();
}
@ -3767,6 +3732,8 @@ function init() {
const loadFill = document.getElementById('loadFill');
const loadPct = document.getElementById('loadPct');
const loadTip = document.getElementById('loadTip');
const loadBg = document.getElementById('loadBg');
if (loadBg) loadBg.src = pickTransitionImage();
// 随机游戏小提示(兼作无声引导,循环轮换)
const TIPS = [
'💡 拖拽空白处可平移视角,滚轮缩放沙盘',
@ -3797,7 +3764,7 @@ function init() {
loadFill.style.width = '100%';
loadPct.textContent = '100%';
// 加载屏至少停留 1 秒:确保主世界元素(地形贴图/居民/建筑)完全就绪再进入,快机也不闪屏
const MIN_LOAD_MS = 1000; // [PLACEHOLDER] 体感1s 是否够?可下调至 800
const MIN_LOAD_MS = 1500; // [PLACEHOLDER] 体感1.5s 是否够?可下调至 1000
const hold = Math.max(0, MIN_LOAD_MS - (Date.now() - loadStart));
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';