mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-17 20:59:58 +08:00
Merge origin/main: preserve relic system and diagnostics updates
This commit is contained in:
@@ -2,6 +2,7 @@ import { bondDefinitions, getBondCombat, getBondDeck }
|
||||
from "../bonds/definitions.js";
|
||||
import { getBondIntents } from "../bonds/intents.js";
|
||||
import { bondProbability, ensureBonds, selectBond } from "../bonds/state.js";
|
||||
import { getDiagnostics } from "../../diagnostics/index.js";
|
||||
|
||||
export function renderBonds(parent, run, saveRun, onBusy = () => {}) {
|
||||
const bonds = ensureBonds(run);
|
||||
@@ -33,6 +34,8 @@ export function renderBonds(parent, run, saveRun, onBusy = () => {}) {
|
||||
updated.querySelector('button[aria-pressed="true"]')
|
||||
?.focus({ preventScroll: true });
|
||||
} catch (error) {
|
||||
getDiagnostics().scope("mengsan.bonds").error("selection.save.failed", error,
|
||||
{ runId: run.runId, revision: run.revision, bondId: id });
|
||||
selectBond(run, previous);
|
||||
status.textContent = "保存失败,请重试。";
|
||||
console.error("梦三助战选择保存失败", error);
|
||||
|
||||
+43
-30
@@ -132,15 +132,26 @@ export function openCardLibrary(run, options = {}) {
|
||||
if (typeof window !== "undefined") window.removeEventListener?.("resize", fitCards);
|
||||
if (dialog.open) dialog.close();
|
||||
dialog.remove();
|
||||
list.replaceChildren(); detailPanel.replaceChildren(); body.replaceChildren();
|
||||
dialog.replaceChildren();
|
||||
catalogRecords = null; descriptionCache = new WeakMap();
|
||||
selectedButton = null; current = [];
|
||||
virtualGrid = null; sizeObserver = null;
|
||||
if (previousFocus?.isConnected) previousFocus.focus();
|
||||
const onClose = options.onClose;
|
||||
options = {}; run = null;
|
||||
onClose?.(close);
|
||||
};
|
||||
button("关闭", header, close);
|
||||
let view = !options.sections && options.view === "catalog" ? "catalog" : "deck";
|
||||
const navigation = element("nav", "ms-deck-tabs", null, dialog);
|
||||
navigation.setAttribute("aria-label", "卡牌列表视图");
|
||||
navigation.hidden = Boolean(options.sections);
|
||||
const deckTab = button("持有牌", navigation, () => { view = "deck"; render(); });
|
||||
const catalogTab = button("图鉴", navigation, () => { view = "catalog"; render(); });
|
||||
deckTab.hidden = catalogTab.hidden = Boolean(options.sections);
|
||||
let sectionIndex = 0;
|
||||
const sectionTabs = options.sections ? options.sections().map((section, index) =>
|
||||
button(section.title, navigation, () => { sectionIndex = index; render(); })) : [];
|
||||
const filters = element("div", "ms-catalog-filters", null, dialog);
|
||||
function field(label) {
|
||||
const wrapper = element("label", "ms-catalog-field", null, filters);
|
||||
@@ -179,13 +190,13 @@ export function openCardLibrary(run, options = {}) {
|
||||
summary.setAttribute("aria-live", "polite");
|
||||
const body = element("div", "ms-deck-body", null, dialog);
|
||||
const footer = element("p", "ms-deck-footer", "点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序", dialog);
|
||||
const current = Array.isArray(run?.player?.deck) ? run.player.deck : [];
|
||||
let detail = false, selectedButton = null, scrollTop = 0;
|
||||
const list = element("div", options.sections ? "ms-deck-grid" : "ms-deck-grid ms-library-grid", null, body);
|
||||
let current = Array.isArray(run?.player?.deck) ? run.player.deck : [];
|
||||
let detail = false, selectedButton = null, scrollTop = 0, orderedView = false;
|
||||
const list = element("div", "ms-deck-grid ms-library-grid", null, body);
|
||||
const detailPanel = element("section", "ms-deck-detail", null, body);
|
||||
detailPanel.hidden = true;
|
||||
let catalogRecords = null, descriptionCache = new WeakMap();
|
||||
if (!options.sections) virtualGrid = createVirtualCardGrid(body, list, createTile);
|
||||
virtualGrid = createVirtualCardGrid(body, list, createTile);
|
||||
function back() {
|
||||
detail = false; detailPanel.hidden = true; list.hidden = false;
|
||||
detailPanel.replaceChildren(); body.scrollTop = scrollTop; fitCards();
|
||||
@@ -214,12 +225,13 @@ export function openCardLibrary(run, options = {}) {
|
||||
body.scrollTop = 0; backButton.focus();
|
||||
}
|
||||
function fitCards() {
|
||||
if (options.sections || closed || !body.clientHeight) return;
|
||||
if (closed || !body.clientHeight) return;
|
||||
const padding = getComputedStyle(body);
|
||||
const available = body.clientHeight - parseFloat(padding.paddingTop) - parseFloat(padding.paddingBottom);
|
||||
const height = Math.max(64, Math.min(218, Math.floor((available - 8) / 2)));
|
||||
const rows = available >= 264 ? 2 : 1;
|
||||
const height = Math.max(128, Math.min(218, Math.floor((available - (rows - 1) * 8) / rows)));
|
||||
dialog.style.setProperty("--ms-card-height", String(height) + "px");
|
||||
dialog.style.setProperty("--ms-card-art-width", String(Math.max(8, Math.min(98, Math.floor((height - 80) * 5 / 7)))) + "px");
|
||||
dialog.style.setProperty("--ms-card-art-width", String(Math.max(8, Math.min(98, Math.floor((height - (orderedView ? 104 : 80)) * 5 / 7)))) + "px");
|
||||
virtualGrid?.layout(height);
|
||||
}
|
||||
function createTile(record, index) {
|
||||
@@ -240,7 +252,6 @@ export function openCardLibrary(run, options = {}) {
|
||||
clearTimeout(searchTimer);
|
||||
detail = false; list.hidden = false; detailPanel.hidden = true;
|
||||
detailPanel.replaceChildren();
|
||||
if (!virtualGrid) list.replaceChildren();
|
||||
body.scrollTop = 0;
|
||||
selectedButton = null;
|
||||
const catalog = !options.sections && view === "catalog";
|
||||
@@ -248,8 +259,22 @@ export function openCardLibrary(run, options = {}) {
|
||||
deckTab.setAttribute("aria-pressed", String(!catalog));
|
||||
catalogTab.setAttribute("aria-pressed", String(catalog));
|
||||
list.classList.toggle("ms-catalog-grid", catalog);
|
||||
let records = [];
|
||||
if (catalog) {
|
||||
let records = [], emptyMessage = "此处暂无卡牌。";
|
||||
orderedView = false;
|
||||
if (options.sections) {
|
||||
const sections = options.sections();
|
||||
const section = sections[sectionIndex];
|
||||
orderedView = Boolean(section?.ordered);
|
||||
if (section?.ordered) emptyMessage = "剩余牌堆为空;下一次摸牌需按规则洗切弃牌堆,洗切后的顺序尚未确定。";
|
||||
sectionTabs.forEach((tab, index) => {
|
||||
tab.textContent = `${sections[index].title} · ${sections[index].cards.length}`;
|
||||
tab.setAttribute("aria-pressed", String(index === sectionIndex));
|
||||
});
|
||||
records = (section?.cards || []).map(card => ({ card, ordered: section.ordered }));
|
||||
summary.textContent = section?.ordered
|
||||
? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。"
|
||||
: `${section?.title || "牌堆"} · 共 ${records.length} 张`;
|
||||
} else if (catalog) {
|
||||
const query = search.value.trim().toLocaleLowerCase();
|
||||
catalogRecords ||= getCardCatalog().map(card => {
|
||||
const details = describeCatalogCard(card);
|
||||
@@ -273,24 +298,11 @@ export function openCardLibrary(run, options = {}) {
|
||||
}
|
||||
footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" :
|
||||
"点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序";
|
||||
if (virtualGrid) {
|
||||
virtualGrid.setRecords(catalog ? records : current.map(card => ({ card })));
|
||||
if (!(catalog ? records : current).length) {
|
||||
element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : "此处暂无卡牌。", list);
|
||||
}
|
||||
fitCards();
|
||||
return;
|
||||
}
|
||||
const sections = catalog ? [{ title: "", cards: records.map(record => record.card) }] :
|
||||
options.sections ? options.sections() : [{ title: "", cards: current }];
|
||||
for (const section of sections) {
|
||||
const cards = section.cards;
|
||||
if (section.title) element("h3", "ms-deck-section", `${section.title} · ${cards.length} 张`, list);
|
||||
if (!cards.length) element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : section.ordered ? "剩余牌堆为空;下一次摸牌需按规则洗切弃牌堆,洗切后的顺序尚未确定。" : "此处暂无卡牌。", list);
|
||||
cards.forEach((card, index) => {
|
||||
list.appendChild(createTile({ card, ordered: section.ordered }, index));
|
||||
});
|
||||
virtualGrid.setRecords(catalog || options.sections ? records : current.map(card => ({ card })));
|
||||
if (!(catalog || options.sections ? records : current).length) {
|
||||
element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : emptyMessage, list);
|
||||
}
|
||||
fitCards();
|
||||
}
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); if (detail) back(); else close(); });
|
||||
dialog.addEventListener("close", close);
|
||||
@@ -300,10 +312,10 @@ export function openCardLibrary(run, options = {}) {
|
||||
document.body.appendChild(dialog);
|
||||
try { dialog.showModal(); } catch (error) { close(); throw error; }
|
||||
fitCards();
|
||||
if (!options.sections && typeof ResizeObserver !== "undefined") {
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
sizeObserver = new ResizeObserver(fitCards);
|
||||
sizeObserver.observe(body);
|
||||
} else if (!options.sections && typeof window !== "undefined") {
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.addEventListener?.("resize", fitCards);
|
||||
}
|
||||
header.querySelector("button").focus();
|
||||
@@ -327,6 +339,7 @@ export function mountBattlePiles(session, battle) {
|
||||
closeDialog?.();
|
||||
closeDialog = openCardLibrary(null, {
|
||||
title: "战斗牌堆", eyebrow: "梦三 · 个人牌堆",
|
||||
onClose(handle) { if (closeDialog === handle) closeDialog = null; },
|
||||
sections: () => [
|
||||
{ title: "剩余牌堆", cards: battle.drawPile, ordered: true },
|
||||
{ title: "弃牌堆", cards: battle.discardPile, ordered: false },
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { getDiagnostics } from "../../../diagnostics/index.js";
|
||||
|
||||
const lines = [];
|
||||
let pendingWrite = Promise.resolve();
|
||||
|
||||
export function createHandDiagnostics(game, sessionId) {
|
||||
const service = getDiagnostics();
|
||||
const diagnostics = service.scope("mengsan.hand", { sessionId });
|
||||
return (message, error) => {
|
||||
if (service.status().enabled) {
|
||||
if (error) diagnostics.error("hand.failed", error, { message });
|
||||
else diagnostics.info("hand.message", { message });
|
||||
return;
|
||||
}
|
||||
const line = `${new Date().toISOString()} [${sessionId}] ${message}` +
|
||||
(error ? ` ${error.message || error}` : "");
|
||||
console[error ? "warn" : "info"](`梦三手牌 UI:${line}`);
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function mountHandUI(player, session, env) {
|
||||
let disposed = false;
|
||||
let observer, resizeObserver, pointer = null, nativeInput = false;
|
||||
let finishStyleLoad = null, refreshTimer = null;
|
||||
let refreshFrame = null;
|
||||
const touchScrollClasses = new Map();
|
||||
let layoutEnabled = false, warned = false;
|
||||
const log = (message, error) => {
|
||||
@@ -72,6 +73,15 @@ export async function mountHandUI(player, session, env) {
|
||||
}
|
||||
overlays.sync(adapter.cards());
|
||||
}
|
||||
function scheduleRefresh() {
|
||||
if (disposed || document.hidden || refreshFrame !== null) return;
|
||||
const draw = () => {
|
||||
refreshFrame = null;
|
||||
if (!document.hidden) refresh();
|
||||
};
|
||||
refreshFrame = typeof window.requestAnimationFrame === "function"
|
||||
? window.requestAnimationFrame(draw) : window.setTimeout(draw, 16);
|
||||
}
|
||||
function listen(target, type, listener, options) {
|
||||
target.addEventListener(type, listener, options);
|
||||
restores.push(() => target.removeEventListener(type, listener, options));
|
||||
@@ -120,6 +130,11 @@ export async function mountHandUI(player, session, env) {
|
||||
if (disposed || !nativeInput) return;
|
||||
nativeInput = false;
|
||||
window.clearTimeout(refreshTimer);
|
||||
if (refreshFrame !== null) {
|
||||
if (typeof window.requestAnimationFrame === "function") window.cancelAnimationFrame(refreshFrame);
|
||||
else window.clearTimeout(refreshFrame);
|
||||
refreshFrame = null;
|
||||
}
|
||||
refreshTimer = window.setTimeout(refresh, 0);
|
||||
}
|
||||
function restoreTouchScroll() {
|
||||
@@ -154,6 +169,11 @@ export async function mountHandUI(player, session, env) {
|
||||
disposed = true;
|
||||
finishStyleLoad?.();
|
||||
window.clearTimeout(refreshTimer);
|
||||
if (refreshFrame !== null) {
|
||||
if (typeof window.requestAnimationFrame === "function") window.cancelAnimationFrame(refreshFrame);
|
||||
else window.clearTimeout(refreshFrame);
|
||||
refreshFrame = null;
|
||||
}
|
||||
restoreTouchScroll();
|
||||
observer?.disconnect();
|
||||
resizeObserver?.disconnect();
|
||||
@@ -201,7 +221,7 @@ export async function mountHandUI(player, session, env) {
|
||||
listen(container, "touchend", () => restoreTouchScroll(), true);
|
||||
listen(container, "touchcancel", () => restoreTouchScroll(), true);
|
||||
}
|
||||
observer = new window.MutationObserver(refresh);
|
||||
observer = new window.MutationObserver(scheduleRefresh);
|
||||
for (const zone of adapter.zones) {
|
||||
observer.observe(zone, { childList: true, subtree: true,
|
||||
attributes: true, attributeFilter: ["class"] });
|
||||
@@ -211,7 +231,7 @@ export async function mountHandUI(player, session, env) {
|
||||
observer.observe(adapter.root.parentNode, { childList: true });
|
||||
}
|
||||
if (window.ResizeObserver && adapter.root) {
|
||||
resizeObserver = new window.ResizeObserver(refresh);
|
||||
resizeObserver = new window.ResizeObserver(scheduleRefresh);
|
||||
resizeObserver.observe(adapter.root);
|
||||
}
|
||||
if (window.PointerEvent && adapter.root) {
|
||||
@@ -221,6 +241,7 @@ export async function mountHandUI(player, session, env) {
|
||||
listen(window, "pointercancel", endInput, true);
|
||||
listen(window, "blur", () => endInput());
|
||||
}
|
||||
listen(document, "visibilitychange", scheduleRefresh);
|
||||
refresh();
|
||||
diagnose(`已挂载手牌 UI,独立排列=${layoutEnabled}`);
|
||||
return { refresh, dispose };
|
||||
|
||||
@@ -2,6 +2,56 @@
|
||||
import { attackHitCount, previewAttackDamage } from "../battle/intent-damage.js";
|
||||
import { isDeathBlowIntent } from "../battle/death-blow.js";
|
||||
import { isStunIntent } from "../battle/stun-intent.js";
|
||||
import { getDiagnostics } from "../../diagnostics/index.js";
|
||||
|
||||
const intentPollers = new WeakMap();
|
||||
const intentFields = ["damage", "hits", "intentType", "summon", "slimed", "infection",
|
||||
"wound", "ringing", "plow", "dazed", "debuff", "stacks", "block", "strength", "sleep", "shrink"];
|
||||
|
||||
export function refreshEnemyIntents(session) {
|
||||
intentPollers.get(session)?.schedule();
|
||||
}
|
||||
|
||||
function observeIntents(session, document, update) {
|
||||
let group = intentPollers.get(session);
|
||||
if (!group) {
|
||||
const callbacks = new Set();
|
||||
const host = document.defaultView || globalThis;
|
||||
let frame = null;
|
||||
const tick = () => {
|
||||
if (document.hidden || session.active === false) return;
|
||||
for (const callback of callbacks) {
|
||||
try { callback(); }
|
||||
catch (error) {
|
||||
getDiagnostics().scope("mengsan.intent").error("refresh.failed", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const schedule = () => {
|
||||
if (frame !== null || document.hidden) return;
|
||||
const draw = () => { frame = null; tick(); };
|
||||
frame = host.requestAnimationFrame ? host.requestAnimationFrame(draw) : host.setTimeout(draw, 16);
|
||||
};
|
||||
const timer = host.setInterval(tick, 500);
|
||||
timer?.unref?.();
|
||||
document.addEventListener?.("visibilitychange", schedule);
|
||||
group = { callbacks, schedule, dispose() {
|
||||
host.clearInterval(timer);
|
||||
if (frame !== null) {
|
||||
if (host.requestAnimationFrame) host.cancelAnimationFrame(frame);
|
||||
else host.clearTimeout(frame);
|
||||
}
|
||||
document.removeEventListener?.("visibilitychange", schedule);
|
||||
intentPollers.delete(session);
|
||||
} };
|
||||
intentPollers.set(session, group);
|
||||
}
|
||||
group.callbacks.add(update);
|
||||
return () => {
|
||||
group.callbacks.delete(update);
|
||||
if (!group.callbacks.size) group.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
export function attackIconForDamage(damage) {
|
||||
if (!Number.isInteger(damage) || damage < 0) {
|
||||
@@ -60,9 +110,9 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
badge.append(damageLine, debuffLine, blockLine, buffLine);
|
||||
player.appendChild(badge);
|
||||
let currentIntent = null;
|
||||
let timer;
|
||||
let previous = null, unsubscribe = () => {};
|
||||
session.ownResource(badge, () => {
|
||||
clearInterval(timer);
|
||||
unsubscribe(); previous = null; currentIntent = null;
|
||||
badge.remove();
|
||||
});
|
||||
const refresh = () => {
|
||||
@@ -180,9 +230,23 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label);
|
||||
badge.hidden = false;
|
||||
};
|
||||
timer = setInterval(refresh, 100);
|
||||
const poll = () => {
|
||||
const values = intentFields.map(key => currentIntent?.[key]);
|
||||
values.push(player.maxHp);
|
||||
if (currentIntent?.damage != null) {
|
||||
const target = getTarget();
|
||||
const targets = Array.isArray(target) ? target : [target];
|
||||
for (const current of targets.length ? targets : [null]) {
|
||||
values.push(previewAttackDamage(currentIntent, player, current));
|
||||
}
|
||||
}
|
||||
if (previous && previous.length === values.length && values.every((value, index) => Object.is(value, previous[index]))) return;
|
||||
refresh(); previous = values;
|
||||
};
|
||||
unsubscribe = observeIntents(session, document, poll);
|
||||
return intent => {
|
||||
currentIntent = intent;
|
||||
refresh();
|
||||
previous = null;
|
||||
poll();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getRelic } from "../../relics/definitions.js";
|
||||
import { createRelicIcon } from "../relic-icon.js";
|
||||
import { renderBonds } from "../bonds.js";
|
||||
import { getSelectableNodes } from "../../progression/state.js";
|
||||
import { getDiagnostics } from "../../../diagnostics/index.js";
|
||||
|
||||
// Original vector pictograms; no external image/font requests.
|
||||
const iconPaths = {
|
||||
@@ -187,6 +188,8 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
|
||||
if (result === "exit") await openModeSelection();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
getDiagnostics().scope("mengsan.map").error("map.operation.failed", error,
|
||||
{ runId: run.runId, revision: run.revision });
|
||||
await askMenu("暂时无法返回", "征程未被清除,请重试。", [{ id: "close", name: "关闭提示" }]);
|
||||
} finally { exitButton.disabled = false; }
|
||||
});
|
||||
@@ -258,7 +261,9 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
|
||||
createText("mengsan-map-menu-action-name-shuying", "卡牌库", copy);
|
||||
createText("mengsan-map-menu-action-description-shuying", `共 ${run.player.deck.length} 张 · 查看花色、点数、属性和卡牌详情`, copy);
|
||||
library.appendChild(copy);
|
||||
library.addEventListener("click", () => { closeCardLibrary = openCardLibrary(run); });
|
||||
library.addEventListener("click", () => { closeCardLibrary = openCardLibrary(run, {
|
||||
onClose(handle) { if (closeCardLibrary === handle) closeCardLibrary = null; },
|
||||
}); });
|
||||
parent.appendChild(library);
|
||||
const appendNames = (title, values, emptyText) => {
|
||||
createText("mengsan-map-inventory-section-title-shuying", title, parent);
|
||||
@@ -308,7 +313,10 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
|
||||
if (back) back.disabled = busy;
|
||||
}), ".mengsan-bonds-shuying button") });
|
||||
createMenuAction({ icon: "book", name: "图鉴", description: "卡牌图鉴 · 查看全部梦三卡牌、效果与费用",
|
||||
onClick: () => { closeCardLibrary = openCardLibrary(run, { view: "catalog", title: "卡牌图鉴", eyebrow: "梦三 · 图鉴" }); }
|
||||
onClick: () => { closeCardLibrary = openCardLibrary(run, {
|
||||
view: "catalog", title: "卡牌图鉴", eyebrow: "梦三 · 图鉴",
|
||||
onClose(handle) { if (closeCardLibrary === handle) closeCardLibrary = null; },
|
||||
}); }
|
||||
}).setAttribute("aria-haspopup", "dialog");
|
||||
createMenuAction({ icon: "record", name: "征程记录", description: `已通过 ${run.statistics?.completedNodes || 0} 个节点`, onClick: () => showMenuPage("征程记录", "本次征程的阶段统计", parent => {
|
||||
const list = ui.create.div(".mengsan-map-record-list-shuying", parent);
|
||||
@@ -326,6 +334,7 @@ export const showMap = (run, { saveRun } = {}) => new Promise(resolve => {
|
||||
try { await openNonameMenu(); }
|
||||
catch (error) {
|
||||
console.error("梦三打开 Noname 菜单失败:", error);
|
||||
getDiagnostics().scope("mengsan.map").error("menu.open.failed", error);
|
||||
await askMenu("暂时无法显示菜单", error.message || "请稍后重试。", [{ id: "close", name: "回到行军图" }]);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { lib, game, ui, _status } from "../../../../noname.js";
|
||||
import { save } from "../../../../noname/util/config.js";
|
||||
import { getDiagnostics } from "../../diagnostics/index.js";
|
||||
|
||||
let selectingMode = false;
|
||||
let nonameMenuTask = null;
|
||||
@@ -50,7 +51,10 @@ export const openNonameMenu = async () => {
|
||||
if (initiallyHidden) {
|
||||
try {
|
||||
if (!container.classList.contains("hidden")) ui.click.configMenu();
|
||||
} catch (closeError) { console.error("Noname 菜单关闭失败:", closeError); }
|
||||
} catch (closeError) {
|
||||
getDiagnostics().scope("mengsan.navigation").error("menu.close.failed", closeError);
|
||||
console.error("Noname 菜单关闭失败:", closeError);
|
||||
}
|
||||
if (wasPaused) game.pause2();
|
||||
else game.resume2();
|
||||
}
|
||||
@@ -170,6 +174,7 @@ export const mountMenu = (overlay, options = {}) => {
|
||||
if (confirmed === "confirm") await openModeSelection();
|
||||
} catch (error) {
|
||||
console.error("梦三菜单操作失败:", error);
|
||||
getDiagnostics().scope("mengsan.navigation").error("menu.operation.failed", error);
|
||||
await askMenu(action === "noname" ? "暂时无法显示菜单" : "暂时无法返回",
|
||||
action === "noname" ? error.message || "请关闭提示后重试。" : "没有清除征程。请关闭提示后重试。",
|
||||
[{ id: "close", name: "回到当前界面" }]);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 保留完整滚动高度,只挂载可见行与上下各两行;滚动时不重建仍可见的卡牌。
|
||||
export function createVirtualCardGrid(viewport, grid, createCard) {
|
||||
const mounted = new Map();
|
||||
const cached = new Map();
|
||||
const maxCached = 24;
|
||||
let records = [], columns = 1, height = 218, gap = 8, padding = 5, viewportHeight = 0;
|
||||
let frame = 0, disposed = false;
|
||||
function draw() {
|
||||
@@ -14,11 +16,14 @@ export function createVirtualCardGrid(viewport, grid, createCard) {
|
||||
for (const [index, node] of mounted) {
|
||||
if ((index < first || index >= end) && node !== document.activeElement) {
|
||||
node.remove(); mounted.delete(index);
|
||||
cached.set(index, node);
|
||||
while (cached.size > maxCached) cached.delete(cached.keys().next().value);
|
||||
}
|
||||
}
|
||||
for (let index = first; index < end; index++) {
|
||||
if (mounted.has(index)) continue;
|
||||
const node = createCard(records[index], index);
|
||||
const node = cached.get(index) || createCard(records[index], index);
|
||||
cached.delete(index);
|
||||
node.dataset.cardIndex = String(index);
|
||||
node.style.gridRow = String(Math.floor(index / columns) + 1);
|
||||
node.style.gridColumn = String(index % columns + 1);
|
||||
@@ -75,7 +80,7 @@ export function createVirtualCardGrid(viewport, grid, createCard) {
|
||||
return {
|
||||
setRecords(value) {
|
||||
// 调用方更新筛选栏后再统一量尺寸,避免用旧高度先生成一批预览。
|
||||
records = value; mounted.clear(); grid.replaceChildren();
|
||||
records = value; mounted.clear(); cached.clear(); grid.replaceChildren();
|
||||
},
|
||||
layout,
|
||||
focus,
|
||||
@@ -84,7 +89,8 @@ export function createVirtualCardGrid(viewport, grid, createCard) {
|
||||
disposed = true; cancelAnimationFrame(frame);
|
||||
viewport.removeEventListener("scroll", schedule);
|
||||
grid.removeEventListener("keydown", keydown);
|
||||
mounted.clear(); records = [];
|
||||
mounted.clear(); cached.clear(); records = [];
|
||||
grid.replaceChildren();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user