- server/:node:http+node:sqlite 零依赖后端(users/sessions/user_stats/codex_unlocks/runs 五表) - 安全:scrypt密码哈希/服务端session/全prepared statement/输入白名单/限流/CSP等安全头/静态白名单防穿越 - 前端:o-auth登陆注册 + o-home飞行员大厅(排行榜Top100三排序+图鉴展柜背景:随机3主3副2僚机+刷怪巡游) - 图鉴改为收集点亮制(未收集=剪影+???),服务端codex_unlocks与本地并集同步 - 修复:回标题/大厅后背景卡在上场残影——render按battleUI门控+离场G=null;空闲=星空 - 结算上报:endRun/rush胜利两处submitRun,Boss击杀计数入排行榜 - playwright全流程验证通过(注册/登陆/游戏/结算/图鉴/安全注入/限流);游戏视觉未验收(用户亲自)
122 lines
4.4 KiB
JavaScript
122 lines
4.4 KiB
JavaScript
"use strict";
|
||
/* ================= 飞行员大厅(主页):排行榜 + 图鉴展柜背景 ================= */
|
||
let LB_SORT = "endless";
|
||
let HOME_SHOW = null;
|
||
|
||
/* 图鉴已收集数(只统计合法 key) */
|
||
function codexCount() {
|
||
let n = 0;
|
||
for (const k in SAVE.codex)
|
||
if (SAVE.codex[k] && (WEAPONS[k] || SUBS[k] || DRONES[k] || PASSIVES[k])) n++;
|
||
return n;
|
||
}
|
||
|
||
async function enterHome() {
|
||
G = null;
|
||
UI = "home";
|
||
rollHomeShow();
|
||
show("o-home");
|
||
/* 刷新账号数据(stats/codex),再渲染面板与排行榜 */
|
||
const r = await NET.api("/api/me", "GET");
|
||
if (r.ok) NET._apply(r.data);
|
||
else if (r.status === 401) { NET.user = null; UI = "auth"; show("o-auth"); return; }
|
||
buildHomeStats();
|
||
buildLeaderboard(LB_SORT);
|
||
}
|
||
|
||
function buildHomeStats() {
|
||
const s = NET.stats || { bossKills: 0, endlessHigh: 0 };
|
||
const nm = NET.user ? NET.user.username : "--";
|
||
$("home-stats").textContent =
|
||
`${nm} · 击坠Boss ${s.bossKills} · 图鉴 ${codexCount()}/${NET.codexTotal} · 无尽最高 ${s.endlessHigh}`;
|
||
}
|
||
|
||
/* ---------- 排行榜 ---------- */
|
||
document.querySelectorAll(".lb-tabs button").forEach((b) => {
|
||
b.onclick = () => {
|
||
LB_SORT = b.dataset.s;
|
||
document.querySelectorAll(".lb-tabs button").forEach((x) => x.classList.toggle("on", x === b));
|
||
SFX.pick();
|
||
buildLeaderboard(LB_SORT);
|
||
};
|
||
});
|
||
|
||
async function buildLeaderboard(sort) {
|
||
const list = $("lb-list");
|
||
list.innerHTML = '<div class="lb-empty">载入中...</div>';
|
||
const r = await NET.api("/api/leaderboard?sort=" + sort, "GET");
|
||
if (!r.ok) { list.innerHTML = '<div class="lb-empty">载入失败</div>'; return; }
|
||
const rows = r.data.rows || [];
|
||
list.innerHTML = "";
|
||
if (!rows.length) { list.innerHTML = '<div class="lb-empty">虚位以待</div>'; return; }
|
||
const meName = NET.user && NET.user.username;
|
||
rows.forEach((row, i) => {
|
||
const d = document.createElement("div");
|
||
d.className = "lb-row" + (meName && row.username === meName ? " me" : "");
|
||
const cells = [String(i + 1), row.username, String(row.bossKills), row.codexN + "/" + r.data.codexTotal, String(row.endlessHigh)];
|
||
for (const c of cells) {
|
||
const s = document.createElement("span");
|
||
s.textContent = c; // textContent 防注入
|
||
d.appendChild(s);
|
||
}
|
||
list.appendChild(d);
|
||
});
|
||
}
|
||
|
||
/* ---------- 大厅背景:随机军械展柜 + 随机刷怪巡游 ---------- */
|
||
const HOME_MOB_TYPES = ["scout", "weaver", "kami", "splitter", "mine", "gunship", "sniper"];
|
||
|
||
function rollHomeShow() {
|
||
HOME_SHOW = {
|
||
m: shuffle(Object.keys(WEAPONS)).slice(0, 3),
|
||
s: shuffle(Object.keys(SUBS)).slice(0, 3),
|
||
d: shuffle(Object.keys(DRONES)).slice(0, 2),
|
||
t: 0, spawnT: 20, mobs: [],
|
||
};
|
||
}
|
||
|
||
/* 由 render() 在 UI==="home" 时每帧调用(dt=1) */
|
||
function drawHomeShowcase(dt) {
|
||
if (!HOME_SHOW || HOME_SHOW.t > 1800) rollHomeShow(); // 30 秒重抽一轮
|
||
const hs = HOME_SHOW;
|
||
hs.t += dt;
|
||
/* 展柜:3主武+3副武+2僚机,两排漂浮 */
|
||
const items = [
|
||
...hs.m.map((k) => ({ d: WEAPONS[k] })),
|
||
...hs.s.map((k) => ({ d: SUBS[k] })),
|
||
...hs.d.map((k) => ({ d: DRONES[k] })),
|
||
];
|
||
ctx.textAlign = "center";
|
||
items.forEach((it, i) => {
|
||
const col = i % 4, row = (i / 4) | 0;
|
||
const x = W / 2 + (col - 1.5) * 56;
|
||
const y = 118 + row * 52 + Math.sin(hs.t * 0.03 + i * 1.7) * 2.5;
|
||
const img = it.d.icon ? ICONS[it.d.icon] : SPR[it.d.spr];
|
||
if (img) {
|
||
ctx.globalAlpha = 0.85;
|
||
ctx.drawImage(img, x - img.width * 1.5, y - img.height * 1.5, img.width * 3, img.height * 3);
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
ctx.font = "6px 'Press Start 2P',monospace";
|
||
ctx.fillStyle = "#7f8cb8";
|
||
ctx.fillText(it.d.name, x, y + 22);
|
||
});
|
||
/* 随机刷怪:从顶部飘入,正弦摆动 */
|
||
hs.spawnT -= dt;
|
||
if (hs.spawnT <= 0 && hs.mobs.length < 8) {
|
||
hs.spawnT = rndi(50, 110);
|
||
const ty = pick(HOME_MOB_TYPES), elite = Math.random() < 0.2;
|
||
const spr = SPR[(elite ? "e_" : "") + ty] || SPR[ty] || SPR.scout;
|
||
hs.mobs.push({ x: rnd(20, W - 20), y: -16, vy: rnd(0.25, 0.6), ph: rnd(0, TAU), amp: rnd(6, 26), spr, sc: elite ? 1.8 : 1.3, t: 0 });
|
||
}
|
||
for (const m of hs.mobs) {
|
||
m.t += dt;
|
||
m.y += m.vy * dt;
|
||
const mx = m.x + Math.sin(m.t * 0.02 + m.ph) * m.amp;
|
||
ctx.globalAlpha = 0.8;
|
||
ctx.drawImage(m.spr, mx - (m.spr.width * m.sc) / 2, m.y - (m.spr.height * m.sc) / 2, m.spr.width * m.sc, m.spr.height * m.sc);
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
hs.mobs = hs.mobs.filter((m) => m.y < H + 24);
|
||
}
|