feat: 沙盘游戏核心运行时文件上线 + 微观世界加载页UI优化
- 提交 world.html 及 assets/(地形贴图/背景/视频/js)、index/login/terrain_preview - world.html: 微观世界入场加载页重构为左下角提示药丸 + 全宽进度条 - 排除 .bak/__bak 备份文件(.gitignore)
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# 备份与临时文件,不进版本库
|
||||
__bak_pre_20260722/
|
||||
*.bak
|
||||
*.bak2
|
||||
*.backup
|
||||
BIN
assets/bg/world_bg_keyart_16x9.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/bg/world_bg_keyart_16x9_clean.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/bg_core.jpg
Normal file
|
After Width: | Height: | Size: 612 KiB |
BIN
assets/bg_earth.jpg
Normal file
|
After Width: | Height: | Size: 699 KiB |
BIN
assets/bg_sky.jpg
Normal file
|
After Width: | Height: | Size: 411 KiB |
BIN
assets/bg_star.jpg
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
assets/bg_under.jpg
Normal file
|
After Width: | Height: | Size: 723 KiB |
42
assets/game-api.js
Normal file
@ -0,0 +1,42 @@
|
||||
// 宇森游戏 · 前端与 Odoo 通信层
|
||||
// 所有接口同源(yt.pengyuthon.cn),由 nginx 代理 /game/* 到 Odoo(game 库)。
|
||||
// 关键点:Odoo 通过 URL 上的 ?db=game 选定数据库(与 game.conf 的 /web/login?db=game 一致),
|
||||
// 否则请求到达时未选中库,控制器路由找不到 → 404。因此所有调用都带 ?db=game。
|
||||
// 登录态通过 Odoo session cookie 保持,fetch 带 credentials:'same-origin'。
|
||||
|
||||
const API = {
|
||||
async me() {
|
||||
return (await fetch('/game/api/me?db=game', { credentials: 'same-origin' })).json();
|
||||
},
|
||||
async login(login, password) {
|
||||
return (await fetch('/game/api/login?db=game', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login, password }),
|
||||
})).json();
|
||||
},
|
||||
async register(payload) {
|
||||
return (await fetch('/game/api/register?db=game', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})).json();
|
||||
},
|
||||
async logout() {
|
||||
return (await fetch('/game/api/logout?db=game', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
})).json();
|
||||
},
|
||||
async codex() {
|
||||
return (await fetch('/game/api/codex?db=game', { credentials: 'same-origin' })).json();
|
||||
},
|
||||
async rules() {
|
||||
return (await fetch('/game/api/rules?db=game', { credentials: 'same-origin' })).json();
|
||||
},
|
||||
async threads() {
|
||||
return (await fetch('/game/api/threads?db=game', { credentials: 'same-origin' })).json();
|
||||
},
|
||||
async announcements() {
|
||||
return (await fetch('/game/api/announcements?db=game', { credentials: 'same-origin' })).json();
|
||||
},
|
||||
};
|
||||
69
assets/site-app.js
Normal file
@ -0,0 +1,69 @@
|
||||
// 宇森官网 · 公开页数据打通(图鉴/规则/论坛/公告 从 Odoo 取数)
|
||||
// 依赖 assets/game-api.js
|
||||
|
||||
// ---------- 规则 ----------
|
||||
async function loadRules(){
|
||||
const el = document.querySelector('#rules .rules-scroll');
|
||||
if (!el) return;
|
||||
try {
|
||||
const d = await API.rules();
|
||||
const groups = (d && d.groups) || [];
|
||||
if (!groups.length){ el.innerHTML = '<div class="tdesc">暂无规则数据</div>'; return; }
|
||||
el.innerHTML = groups.map(g => {
|
||||
const rows = (g.items || []).map(it => {
|
||||
const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
|
||||
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${it.desc || ''}</span></div>`;
|
||||
}).join('');
|
||||
return `<div class="rule-block"><h3>${g.label}</h3><div class="tier-list">${rows}</div></div>`;
|
||||
}).join('');
|
||||
} catch (e) { /* 保留静态兜底内容 */ }
|
||||
}
|
||||
|
||||
// ---------- 论坛 ----------
|
||||
async function loadForum(){
|
||||
const threadsEl = document.getElementById('threads');
|
||||
const catsEl = document.querySelector('#forum .f-cats');
|
||||
if (!threadsEl) return;
|
||||
try {
|
||||
const list = await API.threads();
|
||||
if (!list || !list.length){ threadsEl.innerHTML = '<div class="th"><span class="tt">暂无主题</span></div>'; return; }
|
||||
// 分类筛选
|
||||
const cats = [];
|
||||
list.forEach(t => { if (t.category && cats.indexOf(t.category) < 0) cats.push(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('');
|
||||
catsEl.querySelectorAll('.ch').forEach(b => b.addEventListener('click', () => {
|
||||
catsEl.querySelectorAll('.ch').forEach(x => x.classList.remove('active'));
|
||||
b.classList.add('active');
|
||||
const c = b.dataset.fcat;
|
||||
threadsEl.querySelectorAll('.th').forEach(t => { t.style.display = (c === 'all' || t.dataset.fcat === c) ? '' : 'none'; });
|
||||
}));
|
||||
}
|
||||
threadsEl.innerHTML = list.map(t =>
|
||||
`<div class="th" data-fcat="${t.category || 'all'}">` +
|
||||
(t.pinned ? `<span class="pin">置顶</span>` : '') +
|
||||
`<span class="tt">${t.name}</span>` +
|
||||
`<span class="meta"><span class="rp">${(t.replies || []).length}</span> 回复 · ${t.author_name || '匿名'}</span></div>`
|
||||
).join('');
|
||||
} catch (e) { /* 保留静态兜底内容 */ }
|
||||
}
|
||||
|
||||
// ---------- 公告 ----------
|
||||
async function loadAnnounce(){
|
||||
const el = document.querySelector('#announce .notes');
|
||||
if (!el) return;
|
||||
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('');
|
||||
} catch (e) { /* 保留静态兜底内容 */ }
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadRules();
|
||||
loadForum();
|
||||
loadAnnounce();
|
||||
});
|
||||
BIN
assets/terrain/desert_1.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
assets/terrain/desert_2.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
assets/terrain/desert_3.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/terrain/desert_4.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
assets/terrain/desert_5.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
assets/terrain/desert_6.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
assets/terrain/desert_7.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
assets/terrain/forest_1.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
assets/terrain/forest_2.png
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
assets/terrain/forest_3.png
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
assets/terrain/forest_5.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
assets/terrain/forest_6.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
assets/terrain/forest_7.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
assets/terrain/forest_8.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
60
assets/terrain/manifest.json
Normal file
@ -0,0 +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"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
BIN
assets/terrain/mountain_1.png
Normal file
|
After Width: | Height: | Size: 611 KiB |
BIN
assets/terrain/mountain_2.png
Normal file
|
After Width: | Height: | Size: 577 KiB |
BIN
assets/terrain/mountain_3.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
assets/terrain/mountain_4.png
Normal file
|
After Width: | Height: | Size: 633 KiB |
BIN
assets/terrain/mountain_5.png
Normal file
|
After Width: | Height: | Size: 600 KiB |
BIN
assets/terrain/mountain_6.png
Normal file
|
After Width: | Height: | Size: 465 KiB |
BIN
assets/terrain/mountain_7.png
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
assets/terrain/mountain_8.png
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
assets/terrain/plain_1.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
assets/terrain/plain_2.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
assets/terrain/plain_3.png
Normal file
|
After Width: | Height: | Size: 530 KiB |
BIN
assets/terrain/plain_4.png
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
assets/terrain/plain_5.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
assets/terrain/plain_6.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
assets/terrain/plain_7.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
assets/terrain/plain_8.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
assets/terrain/snow_1.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
assets/terrain/snow_2.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
assets/terrain/snow_3.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
assets/terrain/snow_4.png
Normal file
|
After Width: | Height: | Size: 415 KiB |
BIN
assets/terrain/snow_5.png
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
assets/terrain/snow_6.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
assets/terrain/snow_8.png
Normal file
|
After Width: | Height: | Size: 463 KiB |
BIN
assets/terrain/water_1.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
BIN
assets/terrain/water_2.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
assets/terrain/water_3.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
assets/terrain/water_4.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
assets/terrain/water_5.png
Normal file
|
After Width: | Height: | Size: 405 KiB |
BIN
assets/terrain/water_6.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
assets/terrain/water_7.png
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
assets/terrain/water_8.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
assets/terrain/water_9.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
assets/ui/logo_planet.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/ui/logo_planet_256.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
assets/ui/logo_planet_48.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
164
assets/world-app.js
Normal file
@ -0,0 +1,164 @@
|
||||
// 宇森 · 世界内面板逻辑(图鉴 / 规则 / 论坛 / 公告)
|
||||
// 依赖 assets/game-api.js(提供 API)
|
||||
|
||||
// ---------- 登录态守卫 ----------
|
||||
// TEMP: 本地临时默认放行,跳过 Odoo 登录校验;打通后端后删除此分支并恢复下面逻辑
|
||||
const TEMP_SKIP_AUTH = true;
|
||||
(async () => {
|
||||
if (TEMP_SKIP_AUTH) {
|
||||
const u = document.getElementById('userName');
|
||||
if (u) u.textContent = '园丁';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const me = await API.me();
|
||||
if (!me.uid) { location.href = 'login.html?next=world.html'; return; }
|
||||
const u = document.getElementById('userName');
|
||||
if (u) u.textContent = me.name || me.login;
|
||||
} catch (e) {
|
||||
location.href = 'login.html';
|
||||
}
|
||||
})();
|
||||
|
||||
// ---------- 退出 ----------
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) logoutBtn.addEventListener('click', async () => {
|
||||
await API.logout();
|
||||
location.href = 'index.html';
|
||||
});
|
||||
|
||||
// ---------- 面板控制 ----------
|
||||
const panel = document.getElementById('dataPanel');
|
||||
const dpTitle = document.getElementById('dpTitle');
|
||||
const dpBody = document.getElementById('dpBody');
|
||||
const dpClose = document.getElementById('dpClose');
|
||||
if (dpClose) dpClose.addEventListener('click', () => panel.classList.remove('open'));
|
||||
|
||||
async function openPanel(kind) {
|
||||
if (!panel) return;
|
||||
panel.classList.add('open');
|
||||
dpBody.innerHTML = '<div class="dp-loading">加载中…</div>';
|
||||
try {
|
||||
if (kind === 'codex') { dpTitle.textContent = '图鉴'; renderCodex(await API.codex()); }
|
||||
else if (kind === 'rules') { dpTitle.textContent = '规则'; renderRules(await API.rules()); }
|
||||
else if (kind === 'forum') { dpTitle.textContent = '论坛'; renderForum(await API.threads()); }
|
||||
else if (kind === 'announce') { dpTitle.textContent = '公告'; renderAnnounce(await API.announcements()); }
|
||||
} catch (e) {
|
||||
dpBody.innerHTML = '<div class="dp-loading">加载失败,请稍后重试</div>';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.world-nav button[data-panel]').forEach(b => {
|
||||
b.addEventListener('click', () => openPanel(b.dataset.panel));
|
||||
});
|
||||
|
||||
// ---------- 程序化矢量缩略图(零 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(' ');}
|
||||
function shRace(r,s){const v=Math.floor(r()*3);let x=`<ellipse cx="50" cy="55" rx="19" ry="21" fill="url(#fg${s})" stroke="rgba(255,255,255,.25)" stroke-width="1"/>`;if(v===0){x+=`<path d="M35 40 L31 20 L43 36 Z" fill="url(#fg${s})"/><path d="M65 40 L69 20 L57 36 Z" fill="url(#fg${s})"/>`;}else if(v===1){x+=`<path d="M32 52 L20 42 L36 46 Z" fill="url(#fg${s})"/><path d="M68 52 L80 42 L64 46 Z" fill="url(#fg${s})"/>`;}else{x+=`<path d="M34 36 L42 24 L50 34 L58 24 L66 36 Z" fill="url(#fg${s})"/>`;}x+=`<circle cx="43" cy="55" r="2.6" fill="#0b1020"/><circle cx="57" cy="55" r="2.6" fill="#0b1020"/>`;return x;}
|
||||
function shBuild(r,s){let x=`<rect x="34" y="44" width="32" height="34" rx="2" fill="url(#fg${s})" opacity=".92"/>`;x+=`<path d="M30 44 L50 28 L70 44 Z" fill="url(#fg${s})"/>`;for(let i=0;i<3;i++)x+=`<rect x="${39+i*7}" y="52" width="4" height="7" rx="1" fill="rgba(8,12,26,.7)"/>`;return x;}
|
||||
function shElement(r,s){const sides=3+Math.floor(r()*6);let x=`<polygon points="${polygonPts(50,52,sides,25)}" fill="none" stroke="url(#fg${s})" stroke-width="3"/>`;x+=`<circle cx="50" cy="52" r="7" fill="url(#fg${s})"/>`;return x;}
|
||||
function shPersona(r,s){let x=`<path d="M32 36 Q50 28 68 36 Q72 56 50 70 Q28 56 32 36 Z" fill="url(#fg${s})" opacity=".9"/>`;x+=`<circle cx="43" cy="48" r="2.8" fill="#0b1020"/><circle cx="57" cy="48" r="2.8" fill="#0b1020"/>`;x+=`<path d="M43 60 Q50 64 57 60" stroke="#0b1020" stroke-width="2" fill="none"/>`;return x;}
|
||||
function shTalent(r,s){const sp=4+Math.floor(r()*4);let x=`<polygon points="${starPts(50,52,sp,26,11)}" fill="url(#fg${s})" opacity=".92"/>`;x+=`<circle cx="50" cy="52" r="5" fill="rgba(8,12,26,.55)"/>`;return x;}
|
||||
function shRelic(r,s){let x=`<path d="M50 24 L70 46 L50 76 L30 46 Z" fill="url(#fg${s})" opacity=".9" stroke="rgba(255,255,255,.3)" stroke-width="1"/>`;x+=`<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 x;}
|
||||
function shEquip(sub,s){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${s})"/><rect x="44" y="44" width="12" height="5" rx="2" fill="rgba(8,12,26,.6)"/>`;case 'hand':{let x=`<rect x="36" y="40" width="28" height="22" rx="6" fill="url(#fg${s})"/><rect x="33" y="44" width="9" height="14" rx="3" fill="url(#fg${s})"/>`;for(let i=0;i<3;i++)x+=`<rect x="${40+i*8}" y="62" width="5" height="8" rx="2" fill="url(#fg${s})"/>`;return x;}case 'chest':return `<path d="M30 46 Q50 38 70 46 L70 66 Q50 72 30 66 Z" fill="url(#fg${s})"/><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${s})"/>`;case 'shoes':return `<rect x="34" y="44" width="28" height="6" rx="3" fill="url(#fg${s})"/><path d="M34 48 L60 48 L66 64 L34 64 Z" fill="url(#fg${s})"/>`;case 'ring':return `<circle cx="50" cy="55" r="16" fill="none" stroke="url(#fg${s})" stroke-width="5"/><path d="M50 29 L57 40 L43 40 Z" fill="url(#fg${s})"/>`;case 'neck':return `<path d="M32 42 Q50 64 68 42" fill="none" stroke="url(#fg${s})" stroke-width="3"/><path d="M44 58 L50 72 L56 58 Z" fill="url(#fg${s})"/>`;case 'treasure':return `<rect x="38" y="60" width="24" height="9" rx="2" fill="url(#fg${s})"/><circle cx="50" cy="46" r="14" fill="url(#fg${s})" 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${s})"/>`;}}
|
||||
function shEvent(r,s){let x=`<ellipse cx="50" cy="52" rx="22" ry="28" fill="none" stroke="url(#fg${s})" stroke-width="3"/>`;x+=`<ellipse cx="50" cy="52" rx="12" ry="16" fill="url(#fg${s})" opacity=".5"/>`;return x;}
|
||||
function shapeFor(cat,sub,r,s){switch(cat){case 'race':return shRace(r,s);case 'build':return shBuild(r,s);case 'element':return shElement(r,s);case 'persona':return shPersona(r,s);case 'talent':return shTalent(r,s);case 'relic':return shRelic(r,s);case 'equip':return shEquip(sub,s);case 'event':return shEvent(r,s);default:return `<circle cx="50" cy="50" r="18" fill="url(#fg${s})"/>`;}}
|
||||
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>`;
|
||||
}
|
||||
|
||||
// ---------- 渲染:图鉴(分类 → 子级 → 条目) ----------
|
||||
function renderCodex(data){
|
||||
const groups = (data && data.groups) || [];
|
||||
if (!groups.length){ dpBody.innerHTML = '<div class="dp-empty">暂无图鉴数据</div>'; return; }
|
||||
let html = '';
|
||||
groups.forEach(g => {
|
||||
html += `<div class="dp-block"><h3>${g.label}</h3>`;
|
||||
(g.subs || []).forEach(sub => {
|
||||
if (sub.name && sub.name !== '其他') html += `<div class="dp-sublabel">${sub.name}</div>`;
|
||||
html += '<div class="dp-grid">';
|
||||
sub.items.forEach(it => {
|
||||
const imgTag = it.image
|
||||
? `<img class="dp-thumb-img" src="${it.image}" alt="${it.name}" onerror="this.outerHTML=window.__thumbCache?window.__thumbCache:''" data-cat="${it.cat}" data-sub="${it.sub}" data-name="${it.name}" data-tier="${it.tier}"/>`
|
||||
: '';
|
||||
const fallback = `<div class="dp-thumb-svg">${thumbSVG(it)}</div>`;
|
||||
html += `<div class="dp-card">`+
|
||||
`<div class="dp-thumb">${imgTag || fallback}</div>`+
|
||||
`<div class="dp-card-body"><div class="dp-nm">${it.name}</div>`+
|
||||
`<div class="dp-meta"><span class="dp-sub">${it.sub||''}</span><span class="dp-tier">${it.tier||''}</span></div>`+
|
||||
`<div class="dp-ds">${it.desc||''}</div></div></div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
dpBody.innerHTML = html;
|
||||
// 处理有 image 但加载失败的:替换为程序化 SVG
|
||||
dpBody.querySelectorAll('img.dp-thumb-img').forEach(img => {
|
||||
img.addEventListener('error', () => {
|
||||
const svg = thumbSVG({cat:img.dataset.cat, sub:img.dataset.sub, name:img.dataset.name, tier:img.dataset.tier});
|
||||
const div = document.createElement('div'); div.className='dp-thumb-svg'; div.innerHTML = svg;
|
||||
img.replaceWith(div);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 渲染:规则(按类别分组) ----------
|
||||
function renderRules(data){
|
||||
const groups = (data && data.groups) || [];
|
||||
if (!groups.length){ dpBody.innerHTML = '<div class="dp-empty">暂无规则数据</div>'; return; }
|
||||
let html = '';
|
||||
groups.forEach(g => {
|
||||
html += `<div class="dp-block"><h3>${g.label}</h3><div class="dp-rows">`;
|
||||
(g.items || []).forEach(it => {
|
||||
const lvl = (it.level!=null) ? ('Lv.'+it.level) : '';
|
||||
html += `<div class="dp-row"><span class="dp-lvl">${lvl}</span><span class="dp-rname">${it.name}</span><span class="dp-rdesc">${it.desc||''}</span></div>`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
dpBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---------- 渲染:论坛 ----------
|
||||
function renderForum(threads){
|
||||
threads = threads || [];
|
||||
if (!threads.length){ dpBody.innerHTML = '<div class="dp-empty">暂无主题</div>'; return; }
|
||||
let html = '<div class="dp-threads">';
|
||||
threads.forEach(t => {
|
||||
const replies = (t.replies || []).map(p =>
|
||||
`<div class="dp-reply"><div class="dp-reply-head"><b>${p.author_name||'匿名'}</b><span>${p.create_date}</span></div><div class="dp-reply-body">${p.body_html||''}</div></div>`
|
||||
).join('');
|
||||
html += `<div class="dp-thread">`+
|
||||
`<div class="dp-thread-head"><span class="dp-cat">${t.category_label||''}</span>`+
|
||||
(t.pinned?`<span class="dp-pin">置顶</span>`:'')+`<span class="dp-tt">${t.name}</span>`+
|
||||
`<span class="dp-author">${t.author_name||''}</span></div>`+
|
||||
`<div class="dp-thread-body">${t.body_html||''}</div>`+
|
||||
`<div class="dp-replies">${replies}</div></div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
dpBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---------- 渲染:公告 ----------
|
||||
function renderAnnounce(list){
|
||||
list = list || [];
|
||||
if (!list.length){ dpBody.innerHTML = '<div class="dp-empty">暂无公告</div>'; return; }
|
||||
let html = '<div class="dp-notes">';
|
||||
list.forEach(n => {
|
||||
const pri = n.priority==='urgent' ? 'urgent' : (n.priority==='important' ? 'imp' : '');
|
||||
html += `<div class="dp-note ${pri}"><div class="dp-date">${n.date}</div>`+
|
||||
`<div class="dp-note-body">${n.body_html||''}</div></div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
dpBody.innerHTML = html;
|
||||
}
|
||||
BIN
assets/world_core.mp4
Normal file
BIN
assets/world_earth.mp4
Normal file
BIN
assets/world_sky.mp4
Normal file
BIN
assets/world_star.mp4
Normal file
BIN
assets/world_under.mp4
Normal file
BIN
assets/yusen-trailer.mp4
Normal file
572
index.html
Normal file
@ -0,0 +1,572 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宇森 · Yusen|活的文明系统模拟</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.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" />
|
||||
<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;}
|
||||
.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);}
|
||||
.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.active{display:flex;}
|
||||
|
||||
/* ===== 介绍(视频主页) ===== */
|
||||
#intro{padding:0;}
|
||||
.hero{position:relative;flex:1;border-radius:0;overflow:hidden;}
|
||||
.hero video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
|
||||
.hero .veil{position:absolute;inset:0;background:linear-gradient(110deg, rgba(8,12,26,.80) 0%, rgba(14,23,48,.28) 46%, rgba(8,12,26,.5) 100%);}
|
||||
.hero .copy{position:absolute;left:56px;bottom:48px;right:56px;}
|
||||
.hero .copy h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:64px;letter-spacing:8px;color:#fff;text-shadow:0 0 28px rgba(139,92,246,.8);line-height:1;}
|
||||
.hero .copy .sub2{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--gold);margin:12px 0 16px;letter-spacing:1px;}
|
||||
.hero .copy .tag{font-size:22px;font-weight:500;color:#fff;margin-bottom:9px;}
|
||||
.hero .copy .desc{font-size:15px;color:var(--text-dim);max-width:560px;line-height:1.7;}
|
||||
.hero .copy{z-index:3;pointer-events:none;}
|
||||
.hero .enter-wrap{position:absolute;left:0;right:0;bottom:44px;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;}
|
||||
.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);}
|
||||
.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:row;gap:38px;align-items:stretch;}
|
||||
.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:14px;}
|
||||
.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:22px 44px;gap:0;}
|
||||
.codex-wrap{display:flex;flex-direction:column;gap:13px;height:100%;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(176px,1fr));gap:14px;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 .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);}
|
||||
.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:10px;font-family:"Press Start 2P",monospace;color:var(--gold);}
|
||||
.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;}
|
||||
.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 .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);}
|
||||
|
||||
@media (max-width:900px){
|
||||
.grid{grid-template-columns:repeat(2,1fr);}
|
||||
#background,#forum{flex-direction:column;overflow-y:auto;}
|
||||
.layers,.f-cats{flex:none;}
|
||||
.hero .copy h1{font-size:42px;}
|
||||
.nav button{padding:8px 10px;font-size:13px;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="topbar">
|
||||
<div class="logo"><span class="cn">宇森</span><span class="en">YUSEN</span></div>
|
||||
<nav class="nav">
|
||||
<button data-tab="intro" class="active">介绍</button>
|
||||
<button data-tab="codex">图鉴</button>
|
||||
<button data-tab="rules">规则</button>
|
||||
<button data-tab="forum">论坛</button>
|
||||
<button data-tab="announce">公告</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">
|
||||
<video autoplay muted loop playsinline>
|
||||
<source src="assets/yusen-trailer.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div class="veil"></div>
|
||||
<div class="enter-wrap">
|
||||
<button class="enter-btn" id="enterGame">进入游戏</button>
|
||||
</div>
|
||||
<div class="copy">
|
||||
<h1>宇森</h1>
|
||||
<div class="tag">你不是玩家,你是园丁。</div>
|
||||
<div class="desc">一款活的文明系统模拟——播种一个会自己生长的世界,从地心到陨石星空,见证文明在垂直切面中自发演化。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== 特色 ===== -->
|
||||
|
||||
|
||||
<!-- ===== 背景 ===== -->
|
||||
|
||||
|
||||
<!-- ===== 规则 ===== -->
|
||||
<section class="panel" id="rules">
|
||||
<div class="sec-title">等级制度<small>TIERS & RANK SYSTEM</small></div>
|
||||
<div class="rules-scroll">
|
||||
<div class="rule-block">
|
||||
<h3>① 世界等级 · 五阶段</h3>
|
||||
<p class="rule-intro">世界从蒙昧到飞升的整体进化阶段,决定可解锁的内容深度与事件池上限。园丁对世界施加的影响会推动阶段跃迁——这是文明能"发生什么"的舞台上限。</p>
|
||||
<div class="tier-list">
|
||||
<div class="tier-row"><span class="tn">阶段一</span><span class="tname">蒙昧</span><span class="tdesc">文明萌芽,仅基础采集与居住,无势力概念,事件以自然为主。</span></div>
|
||||
<div class="tier-row"><span class="tn">阶段二</span><span class="tname">聚落</span><span class="tdesc">出现村落与分工,解锁基础建造与贸易,可建立首个势力。</span></div>
|
||||
<div class="tier-row"><span class="tn">阶段三</span><span class="tname">王国</span><span class="tdesc">统一政权成形,解锁军事/宗教/科技分支,传奇事件开始频现。</span></div>
|
||||
<div class="tier-row"><span class="tn">阶段四</span><span class="tname">文明</span><span class="tdesc">跨层互通成熟,地下与星空纳入治理,神器与装备体系全开。</span></div>
|
||||
<div class="tier-row"><span class="tn">阶段五</span><span class="tname">飞升</span><span class="tdesc">文明触及世界本质,可开启飞升轮回,进入更高维的演化实验。</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-block">
|
||||
<h3>② 势力等级</h3>
|
||||
<p class="rule-intro">文明内部群体的聚合度,由人口、领土、影响力累计。势力等级越高,可调动的世界资源与外交权重越大——决定文明"能调动什么"。</p>
|
||||
<div class="tier-list">
|
||||
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">部族</span><span class="tdesc">血缘纽带的小群体,无正式制度。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">城邦</span><span class="tdesc">多聚落联盟,出现雏形治理与税收。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">公国</span><span class="tdesc">领地整合,常备军与法典成型。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">王国</span><span class="tdesc">统一王朝,跨层资源调度能力。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">帝国</span><span class="tdesc">大陆级霸权,可发起跨世界行动。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">星盟</span><span class="tdesc">联结多方世界,参与宏观文明博弈。</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-block">
|
||||
<h3>③ 人物角色等级 · 十二阶段</h3>
|
||||
<p class="rule-intro">个体(英雄 / 领袖 / 特殊 NPC)的成长阶序,通过历练、天赋点亮与装备积累提升,影响可承担的职责与战力——决定个体"能做成什么"。</p>
|
||||
<div class="tier-list">
|
||||
<div class="tier-row"><span class="tn">Lv.1</span><span class="tname">萌新</span><span class="tdesc">初入世界的无名者。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.2</span><span class="tname">学徒</span><span class="tdesc">掌握基础技艺。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.3</span><span class="tname">熟手</span><span class="tdesc">可独立完成任务。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.4</span><span class="tname">好手</span><span class="tdesc">在某领域崭露头角。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.5</span><span class="tname">精英</span><span class="tdesc">群体中的中坚。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.6</span><span class="tname">大师</span><span class="tdesc">技艺臻于化境。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.7</span><span class="tname">宗匠</span><span class="tdesc">开宗立派的人物。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.8</span><span class="tname">传奇</span><span class="tdesc">事迹被载入编年史。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.9</span><span class="tname">神话</span><span class="tdesc">近乎传说的存在。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.10</span><span class="tname">半神</span><span class="tdesc">触及世界法则。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.11</span><span class="tname">圣者</span><span class="tdesc">被信仰托举的意志。</span></div>
|
||||
<div class="tier-row"><span class="tn">Lv.12</span><span class="tname">永恒</span><span class="tdesc">超越生死的世界锚点。</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-block">
|
||||
<h3>④ 制度说明</h3>
|
||||
<p class="rule-intro">三者相互独立又彼此加成:<b style="color:#fff">世界等级</b>是舞台上限,决定能发生什么;<b style="color:#fff">势力等级</b>是群体进度,决定能调动什么;<b style="color:#fff">角色等级</b>是个体成长,决定能做成什么。园丁的核心策略,是在三者间分配有限的引导资源,让文明自洽地向上演化。<br><br>跃迁逻辑:角色历练与装备积累→提升势力实力→推动世界阶段跃迁→解锁更深内容(更深的地下层、更远的星空层、更强的神器)。每一级提升都会扩大可叙事的事件池,世界因此越活越厚。</p>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
<div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div>
|
||||
</div>
|
||||
<div class="threads" id="threads">
|
||||
<div class="th" data-fcat="discuss"><span class="pin">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="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="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>
|
||||
</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>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="codex-data.js"></script>
|
||||
<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');
|
||||
}));
|
||||
|
||||
// ============ 程序化矢量缩略图(零 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 用于唯一渐变 id;r 为确定性随机)
|
||||
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');
|
||||
document.getElementById('newPost').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');
|
||||
async function refreshUserBox(){
|
||||
if(!userBox) return;
|
||||
try{
|
||||
const me = await API.me();
|
||||
if(me && me.uid){
|
||||
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;
|
||||
const lb = document.getElementById('ubLogout');
|
||||
if(lb) lb.addEventListener('click', async ()=>{
|
||||
await API.logout();
|
||||
location.href = 'index.html';
|
||||
});
|
||||
} else {
|
||||
userBox.innerHTML = ''; // 未登录:右上角不显示登录入口,登录经“进入游戏”引导
|
||||
}
|
||||
}catch(e){ userBox.innerHTML = ''; }
|
||||
}
|
||||
refreshUserBox();
|
||||
|
||||
// ============ 进入游戏:已登录→世界页;未登录→提示登录 ============
|
||||
// TEMP: 本地临时默认直接进入游戏,跳过 Odoo 登录校验;打通后端后改回 API.me() 判断
|
||||
const enterBtn = document.getElementById('enterGame');
|
||||
if(enterBtn) enterBtn.addEventListener('click', ()=>{
|
||||
location.href = 'world.html';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
193
login.html
Normal file
@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宇森 · 登录</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+QingKe+HuangYou&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<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,.05); --card-bd:rgba(255,255,255,.12);
|
||||
--glow:0 0 22px rgba(139,92,246,.30);
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
html,body{height:100%;}
|
||||
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,.16), transparent 60%),
|
||||
radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.12), transparent 55%),
|
||||
linear-gradient(160deg, var(--bg-0), var(--bg-1) 48%, var(--bg-2));
|
||||
display:flex;align-items:center;justify-content:center;min-height:100vh;overflow:hidden;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:f1 20s ease-in-out infinite;}
|
||||
body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:f2 24s ease-in-out infinite;}
|
||||
@keyframes f1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
|
||||
@keyframes f2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
|
||||
|
||||
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
|
||||
.brand{text-align:center;margin-bottom:26px;}
|
||||
.brand .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:38px;letter-spacing:6px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.6);}
|
||||
.brand .en{font-size:11px;color:var(--magenta);letter-spacing:3px;margin-top:6px;}
|
||||
.brand .tag{font-size:13px;color:var(--text-dim);margin-top:10px;}
|
||||
|
||||
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:18px;padding:26px 26px 30px;backdrop-filter:blur(10px);box-shadow:var(--glow);}
|
||||
.tabs{display:flex;gap:6px;margin-bottom:20px;background:rgba(8,12,26,.5);border-radius:12px;padding:5px;}
|
||||
.tabs button{flex:1;font-family:inherit;font-size:14px;font-weight:500;color:var(--text-dim);background:transparent;border:none;padding:9px;border-radius:9px;cursor:pointer;transition:.2s;}
|
||||
.tabs button.active{color:#fff;background:rgba(139,92,246,.28);box-shadow:var(--glow);}
|
||||
|
||||
.field{margin-bottom:14px;}
|
||||
.field label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:6px;letter-spacing:1px;}
|
||||
.field input{width:100%;background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:11px 13px;font-family:inherit;font-size:14px;outline:none;transition:.2s;}
|
||||
.field input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(139,92,246,.18);}
|
||||
|
||||
.submit{width:100%;margin-top:6px;font-size:15px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:12px;border-radius:11px;cursor:pointer;border:none;letter-spacing:2px;transition:.22s;box-shadow:0 0 18px rgba(231,184,92,.35);}
|
||||
.submit:hover{filter:brightness(1.08);}
|
||||
.submit:disabled{opacity:.6;cursor:default;}
|
||||
|
||||
.msg{margin-top:14px;font-size:13px;text-align:center;min-height:18px;}
|
||||
.msg.err{color:#ff8a8a;}
|
||||
.msg.ok{color:#8be39a;}
|
||||
|
||||
.back{text-align:center;margin-top:18px;font-size:13px;color:var(--text-dim);}
|
||||
.back a{color:var(--cyan);text-decoration:none;}
|
||||
.back a:hover{text-decoration:underline;}
|
||||
|
||||
.hint{margin-bottom:16px;font-size:13px;color:var(--cyan);background:rgba(76,201,240,.10);border:1px solid rgba(76,201,240,.30);border-radius:10px;padding:9px 12px;text-align:center;}
|
||||
|
||||
.hidden{display:none;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="brand">
|
||||
<div class="cn">宇森</div>
|
||||
<div class="en">YUSEN · GARDEN OF CIVILIZATION</div>
|
||||
<div class="tag">你不是玩家,你是园丁。</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="hint hidden" id="hint">登录后即可进入游戏</div>
|
||||
<div class="tabs">
|
||||
<button id="tabLogin" class="active">登录</button>
|
||||
<button id="tabReg">注册</button>
|
||||
</div>
|
||||
|
||||
<!-- 登录 -->
|
||||
<form id="loginForm" autocomplete="off">
|
||||
<div class="field">
|
||||
<label>账号</label>
|
||||
<input id="lLogin" type="text" placeholder="Odoo 用户名 / 邮箱" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
<input id="lPwd" type="password" placeholder="密码" />
|
||||
</div>
|
||||
<button class="submit" type="submit">进入世界</button>
|
||||
</form>
|
||||
|
||||
<!-- 注册 -->
|
||||
<form id="regForm" class="hidden" autocomplete="off">
|
||||
<div class="field">
|
||||
<label>昵称</label>
|
||||
<input id="rName" type="text" placeholder="显示名称(可空)" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>账号</label>
|
||||
<input id="rLogin" type="text" placeholder="登录用账号" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>邮箱</label>
|
||||
<input id="rEmail" type="email" placeholder="邮箱(可空)" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
<input id="rPwd" type="password" placeholder="至少 6 位" />
|
||||
</div>
|
||||
<button class="submit" type="submit">创建园丁账号</button>
|
||||
</form>
|
||||
|
||||
<div class="msg" id="msg"></div>
|
||||
</div>
|
||||
|
||||
<div class="back"><a href="index.html">← 返回官网</a></div>
|
||||
</div>
|
||||
|
||||
<script src="assets/game-api.js"></script>
|
||||
<script>
|
||||
const tabLogin = document.getElementById('tabLogin');
|
||||
const tabReg = document.getElementById('tabReg');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const regForm = document.getElementById('regForm');
|
||||
const msg = document.getElementById('msg');
|
||||
|
||||
// 回跳目标(进入游戏流程带 ?next=world.html)
|
||||
const params = new URLSearchParams(location.search);
|
||||
const nextUrl = params.get('next') || 'world.html';
|
||||
const hint = document.getElementById('hint');
|
||||
if (params.get('next')) hint.classList.remove('hidden');
|
||||
// 已登录则直接跳走
|
||||
(async () => {
|
||||
try { const me = await API.me(); if (me && me.uid) location.href = nextUrl; } catch (e) {}
|
||||
})();
|
||||
|
||||
function showMsg(text, kind){ msg.textContent = text || ''; msg.className = 'msg' + (kind ? ' ' + kind : ''); }
|
||||
function switchTab(which){
|
||||
if (which === 'login'){
|
||||
tabLogin.classList.add('active'); tabReg.classList.remove('active');
|
||||
loginForm.classList.remove('hidden'); regForm.classList.add('hidden');
|
||||
} else {
|
||||
tabReg.classList.add('active'); tabLogin.classList.remove('active');
|
||||
regForm.classList.remove('hidden'); loginForm.classList.add('hidden');
|
||||
}
|
||||
showMsg('');
|
||||
}
|
||||
tabLogin.onclick = () => switchTab('login');
|
||||
tabReg.onclick = () => switchTab('reg');
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
showMsg('');
|
||||
const login = document.getElementById('lLogin').value.trim();
|
||||
const password = document.getElementById('lPwd').value;
|
||||
if (!login || !password){ showMsg('请填写账号和密码', 'err'); return; }
|
||||
const btn = loginForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '登录中…';
|
||||
try {
|
||||
const r = await API.login(login, password);
|
||||
if (r.ok){ location.href = nextUrl; }
|
||||
else { showMsg(r.error || '登录失败', 'err'); }
|
||||
} catch (err) {
|
||||
showMsg('网络错误,请稍后重试', 'err');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '进入世界';
|
||||
}
|
||||
});
|
||||
|
||||
regForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
showMsg('');
|
||||
const payload = {
|
||||
name: document.getElementById('rName').value.trim(),
|
||||
login: document.getElementById('rLogin').value.trim(),
|
||||
email: document.getElementById('rEmail').value.trim(),
|
||||
password: document.getElementById('rPwd').value,
|
||||
};
|
||||
if (!payload.login || !payload.password){ showMsg('账号和密码必填', 'err'); return; }
|
||||
const btn = regForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '注册中…';
|
||||
try {
|
||||
const r = await API.register(payload);
|
||||
if (r.ok){ location.href = nextUrl; }
|
||||
else { showMsg(r.error || '注册失败', 'err'); }
|
||||
} catch (err) {
|
||||
showMsg('网络错误,请稍后重试', 'err');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '创建园丁账号';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
154
terrain_preview.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>草地变体预览</title><style>
|
||||
body{background:#16181d;color:#e8e8e8;font-family:-apple-system,"PingFang SC",sans-serif;margin:0;padding:24px}
|
||||
h1{font-size:20px;margin:0 0 6px}.hint{color:#9aa0a6;font-size:13px;margin-bottom:20px;line-height:1.6}
|
||||
.section{font-size:15px;color:#7cc4ff;margin:24px 0 12px;font-weight:600}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:16px}
|
||||
.card{background:#22252c;border-radius:10px;overflow:hidden;border:1px solid #333842}
|
||||
.vid-wrap{position:relative;aspect-ratio:1/1;background:#000}
|
||||
.vid-wrap.static{background:repeating-linear-gradient(45deg,#1b1e24,#1b1e24 10px,#20242c 10px,#20242c 20px)}
|
||||
video{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.num{position:absolute;top:8px;left:8px;background:rgba(0,0,0,.65);color:#fff;font-weight:700;padding:3px 9px;border-radius:6px;font-size:14px}
|
||||
.row{display:flex;align-items:center;gap:10px;padding:10px}.row img{width:54px;height:54px;object-fit:cover;border-radius:6px;border:1px solid #3a3f48}
|
||||
.tag{font-size:12px;color:#9aa0a6}</style></head><body>
|
||||
<h1>草地变体预览</h1><div class="hint">视频为动态循环播放(与游戏内一致)。扫一眼,哪个不顺眼就告诉我编号,我直接删。</div>
|
||||
<div class="section">动态视频变体(游戏中实际使用的 7 个)</div><div class="grid"><div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#2</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_2.png" alt="#2">
|
||||
<span class="tag">plain_2.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#4</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_4.png" alt="#4">
|
||||
<span class="tag">plain_4.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#5</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_5.png" alt="#5">
|
||||
<span class="tag">plain_5.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#8</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_8.png" alt="#8">
|
||||
<span class="tag">plain_8.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#12</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_12.png" alt="#12">
|
||||
<span class="tag">plain_12.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#13</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_13.png" alt="#13">
|
||||
<span class="tag">plain_13.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap">
|
||||
<span class="num">#14</span>
|
||||
<video src="assets/terrain/plain_%d.mp4" muted loop autoplay playsinline></video>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_14.png" alt="#14">
|
||||
<span class="tag">plain_14.png</span>
|
||||
</div>
|
||||
</div></div><div class="section">静态 PNG 变体(含 #16 兜底,共 8 个)</div><div class="grid"><div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#2</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_2.png" alt="#2">
|
||||
<span class="tag">plain_2.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#4</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_4.png" alt="#4">
|
||||
<span class="tag">plain_4.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#5</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_5.png" alt="#5">
|
||||
<span class="tag">plain_5.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#8</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_8.png" alt="#8">
|
||||
<span class="tag">plain_8.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#12</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_12.png" alt="#12">
|
||||
<span class="tag">plain_12.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#13</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_13.png" alt="#13">
|
||||
<span class="tag">plain_13.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#14</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_14.png" alt="#14">
|
||||
<span class="tag">plain_14.png</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="vid-wrap static">
|
||||
<span class="num">#16</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img src="assets/terrain/plain_16.png" alt="#16">
|
||||
<span class="tag">plain_16.png</span>
|
||||
</div>
|
||||
</div></div></body></html>
|
||||