- 提交 world.html 及 assets/(地形贴图/背景/视频/js)、index/login/terrain_preview - world.html: 微观世界入场加载页重构为左下角提示药丸 + 全宽进度条 - 排除 .bak/__bak 备份文件(.gitignore)
165 lines
12 KiB
JavaScript
165 lines
12 KiB
JavaScript
// 宇森 · 世界内面板逻辑(图鉴 / 规则 / 论坛 / 公告)
|
||
// 依赖 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;
|
||
}
|