starraid/star-raid/js/home.js
JOJO ab403f4cc7 后端化:账号系统+排行榜+图鉴收集同步+飞行员大厅(零依赖Node+SQLite)
- 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全流程验证通过(注册/登陆/游戏/结算/图鉴/安全注入/限流);游戏视觉未验收(用户亲自)
2026-08-06 04:20:25 +08:00

122 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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);
}