Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28f70964ca | ||
|
|
279f2d9b0a | ||
|
|
7047e10fda | ||
|
|
1c93c77f4f | ||
|
|
0205bbebc6 | ||
|
|
63dc8c66f9 | ||
|
|
cba7b5fbcc | ||
|
|
623f53a1d2 | ||
|
|
3a3d547ccd | ||
|
|
6a9350d06c | ||
|
|
036f929862 | ||
|
|
205a0174ae | ||
|
|
3ac3b5e52a | ||
|
|
d11cb8129a | ||
|
|
3e6e4679f1 | ||
|
|
bb46c05cf6 | ||
|
|
d47ddf9ad4 | ||
|
|
9e1732d2f8 | ||
|
|
d0e0792538 |
@@ -16,25 +16,25 @@ tools/*.mjs
|
||||
|
||||
*.mjs
|
||||
*.cjs
|
||||
!mengsan/tools/check-sts2-relics.mjs
|
||||
!mengsan/tools/write-sts2-relics-report.mjs
|
||||
|
||||
# Local updater downloads and runtime logs
|
||||
/.update_tmp/
|
||||
/.planning/
|
||||
/mengsan/logs/
|
||||
/logs/
|
||||
*.log
|
||||
|
||||
# Local credentials; keep shareable environment templates
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.sample
|
||||
!.env.template
|
||||
|
||||
# Dependency and interpreter caches
|
||||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Editor temporary files and OS metadata
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
@@ -42,3 +42,7 @@ __pycache__/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
/.comet/
|
||||
/.comet/*
|
||||
/.comet/config.yaml
|
||||
|
||||
@@ -650,23 +650,10 @@ const skills = {
|
||||
subSkill: {
|
||||
init: {
|
||||
charlotte: true,
|
||||
forced: true,
|
||||
locked: false,
|
||||
audio: false,
|
||||
trigger: {
|
||||
player: "enterGame",
|
||||
global: "phaseBefore",
|
||||
},
|
||||
filter(event, player) {
|
||||
if (!player.countCharge(true)) {
|
||||
return false;
|
||||
}
|
||||
return event.name != "phase" || game.phaseNumber == 0;
|
||||
},
|
||||
async content(event, trigger, player) {
|
||||
init(player) {
|
||||
const num = get.info("fenwei_shuying").beginMarkCount;
|
||||
player.addCharge(num);
|
||||
await game.delayx();
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# 术樱包自动诊断日志
|
||||
|
||||
扩展加载后自动记录本扩展的关键业务步骤,发生错误时自动生成独立报告。
|
||||
无需开启控制台或手动开始录制;不上传任何数据。
|
||||
|
||||
## 文件位置
|
||||
|
||||
PC 和安卓客户端均写入游戏目录下的 `extension/术樱包/logs/`。
|
||||
|
||||
- `runtime-*.jsonl`:按行保存时间、模块、步骤、关联 ID 和上下文。
|
||||
- `report-*.json`:错误报告,包含首次/最近异常、错误栈、cause、内部异常、
|
||||
运行版本、错误前操作、当前结算阶段和存档状态。
|
||||
|
||||
同一异常在存档层、结算层等多个入口被观察时,共用报告编号并更新修订号;
|
||||
occurrences 表示被捕获的次数,不表示奖励发放次数。最近报告不会因普通日志
|
||||
轮转被删除。保存回执绑定对应修订和真实通道,旧版本写入成功不会误标新版本。
|
||||
|
||||
扩展设置中的“诊断日志位置”显示实际位置和最近报告保存状态。
|
||||
只需要保留相应报告和附近的 runtime 文件;如何发送由用户自行处理。
|
||||
|
||||
## 采集范围
|
||||
|
||||
- 主动记录术樱包加载、更新、梦三战斗、结算、存档、手牌、地图及菜单错误。
|
||||
- 全局错误仅接收来源文件或异常栈属于本扩展的情况,包含编码后的中文路径。
|
||||
- 不因为当前开启梦三就收集所有错误,也不根据错误消息包含“术樱包”归因。
|
||||
- 原引擎错误处理仍会执行;不替换引擎事件、Promise 或 console 的原型。
|
||||
|
||||
## 存档诊断
|
||||
|
||||
报告分别记录 storageStatus、memoryPublished 和 commitReceipt:
|
||||
|
||||
- storageStatus=committed 表示存档事务完成,或 localStorage 写入已返回。
|
||||
- memoryPublished=true 表示运行内存已发布成功存档。
|
||||
- commitReceipt=true 表示结算协调器收到了提交成功结果。
|
||||
|
||||
资源清理、技能释放或地图展示随后失败时,不能据此判断存档失败。
|
||||
重试沿用现有去重和奖励提交规则,日志不参与存档事务,也不触发自动重试。
|
||||
|
||||
## 容量与兼容
|
||||
|
||||
默认每秒批量保存普通日志;错误报告立即排入写入队列。运行日志按约
|
||||
256 KiB 分段;内存保留最近约 512 KiB / 400 条记录。日志目录自动清理
|
||||
系统自己生成的旧文件,保留总量约 5 MiB、最多 64 个文件。报告包含截断和
|
||||
丢弃计数;“完整诊断”指受限的错误链与现场记录,不是无限保留整个游戏过程。
|
||||
|
||||
待写队列默认最多 12 项、合计 2 MiB,优先保存错误报告;另有最多一项正在写入。
|
||||
错误报告复用已脱敏的记录文本,按字节预算一次裁剪,保留首次和最近异常。
|
||||
|
||||
PC 使用异步文件操作,安卓使用 Cordova 目录和 FileWriter 回调,写入前截断旧
|
||||
内容,使用 UTF-8 Blob。单次保存操作设置 5 秒观察超时;超时后停止使用该
|
||||
文件通道,避免迟到写入与后续写入重叠。写入异常不会中断游戏业务。
|
||||
|
||||
原生文件不可用时,降级到独立的 `shuying-diagnostics` IndexedDB;它不与
|
||||
游戏存档共用数据库。若两种持久化方式都不可用,仅保留本次运行的内存
|
||||
记录,菜单会明确显示未持久化,不能宣称文件已生成。
|
||||
|
||||
切入后台或页面隐藏时尝试保存缓冲;被系统强制杀死或断电时,最后尚未完成的
|
||||
异步写入可能丢失。结算内部等待超过 15 秒会记录观察日志,选择奖励、地图节点
|
||||
和剧情等待不会被当作超时错误;观察器不会结束业务事件。
|
||||
|
||||
字段过滤会隐藏凭据字段、常见 URL 凭据参数和 Bearer 内容。报告不会复制完整
|
||||
玩家对象、DOM、引擎状态或整个存档。日志文件不进入发布清单和 Git。
|
||||
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
byteLength, cleanText, extensionError, extensionSource, serializeError, snapshot,
|
||||
} from "./serialize.js";
|
||||
import { createDiagnosticStorage, isDiagnosticFile, LOG_DIRECTORY } from "./storage.js";
|
||||
|
||||
const noop = () => null;
|
||||
const inactive = Object.freeze({
|
||||
info: noop, warn: noop, error: noop, scope: () => inactive,
|
||||
registerContext: () => noop, flush: async () => {},
|
||||
step: async (_event, action) => action(),
|
||||
status: () => ({ enabled: false }), dispose: noop,
|
||||
});
|
||||
let current = inactive;
|
||||
|
||||
export function getDiagnostics() { return current; }
|
||||
|
||||
export function createDiagnostics(options = {}) {
|
||||
const env = options.env || {};
|
||||
const now = options.now || Date.now;
|
||||
const monotonic = () => {
|
||||
try { return env.performance?.now?.() ?? now(); } catch { return now(); }
|
||||
};
|
||||
const launchId = `${now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const maxMemoryBytes = options.maxMemoryBytes ?? 512 * 1024;
|
||||
const maxJournalBytes = options.maxJournalBytes ?? 256 * 1024;
|
||||
const maxReportBytes = options.maxReportBytes ?? 512 * 1024;
|
||||
const maxDiskBytes = options.maxDiskBytes ?? 5 * 1024 * 1024;
|
||||
const maxFiles = options.maxFiles ?? 64;
|
||||
const maxPending = options.maxPending ?? 12;
|
||||
const maxPendingBytes = options.maxPendingBytes ?? 2 * 1024 * 1024;
|
||||
const flushMs = options.flushMs ?? 1000;
|
||||
const recent = [], contexts = new Map(), pending = new Map();
|
||||
const errors = new WeakMap(), fingerprints = new Map(), listeners = [];
|
||||
let sequence = 0, reportSequence = 0, segment = 0, memoryBytes = 0;
|
||||
let journal = [], journalBytes = 0, revision = 0, queuedRevision = -1;
|
||||
let flushTimer = null, worker = null, stopped = false, unavailable = false;
|
||||
let latestReport = null, droppedRecords = 0, droppedWrites = 0;
|
||||
let inventory = null, inventoryKind = null;
|
||||
let pendingBytes = 0;
|
||||
const metadata = snapshot({
|
||||
extension: "术樱包", extensionVersion: options.version || "unknown",
|
||||
engineVersion: env.lib?.version || "unknown", launchId,
|
||||
platform: env.lib?.node ? "pc" : env.lib?.device ? "android" : "browser",
|
||||
userAgent: env.window?.navigator?.userAgent || "unknown",
|
||||
modules: { mengsan: env.lib?.config?.shuYing_mengsan_installed_version || null },
|
||||
});
|
||||
const roots = [];
|
||||
try {
|
||||
const root = new URL("../", import.meta.url);
|
||||
if (["file:", "http:", "https:"].includes(root.protocol)) roots.push(root.href);
|
||||
} catch {}
|
||||
if (options.extensionRoot) {
|
||||
try {
|
||||
roots.push(new URL(options.extensionRoot.replace(/\/?$/, "/"),
|
||||
env.document?.baseURI || env.window?.location?.href).href);
|
||||
} catch {}
|
||||
}
|
||||
const storage = options.storage || createDiagnosticStorage(env, {
|
||||
timeoutMs: options.storageTimeoutMs,
|
||||
onFailure(error, phase) {
|
||||
record("WARN", "diagnostics.storage.failure", { phase }, error, "diagnostics");
|
||||
},
|
||||
});
|
||||
const storageStatus = () => storage.status?.() || { kind: "file", location: LOG_DIRECTORY };
|
||||
const runtimeName = () => `runtime-${launchId}-${segment}.jsonl`;
|
||||
|
||||
function contextFor(extra) {
|
||||
const values = {};
|
||||
for (const [name, provider] of contexts) {
|
||||
try { values[name] = snapshot(provider()); }
|
||||
catch { values[name] = { diagnosticContextUnavailable: true }; }
|
||||
}
|
||||
return { ...values, ...snapshot(extra || {}) };
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
if (flushTimer || stopped || unavailable) return;
|
||||
flushTimer = setTimeout(() => { flushTimer = null; void flush(); }, flushMs);
|
||||
flushTimer?.unref?.();
|
||||
}
|
||||
|
||||
function record(level, event, extra, error, module = "extension") {
|
||||
if (stopped) return null;
|
||||
try {
|
||||
const entry = {
|
||||
schemaVersion: 1, sequence: ++sequence, time: new Date(now()).toISOString(),
|
||||
launchId, level, module: cleanText(module, 128), event: cleanText(event, 256),
|
||||
context: contextFor(extra),
|
||||
};
|
||||
if (error !== undefined) entry.error = serializeError(error);
|
||||
const item = { entry, bytes: 0, text: "" };
|
||||
recent.push(item);
|
||||
if (level === "ERROR") createReport(entry, error);
|
||||
const text = JSON.stringify(entry);
|
||||
const bytes = byteLength(text) + 1;
|
||||
item.text = text;
|
||||
delete item.entry;
|
||||
item.bytes = bytes; memoryBytes += bytes;
|
||||
if (journalBytes + bytes > maxJournalBytes && journal.length) {
|
||||
enqueue(runtimeName(), journal.join(""));
|
||||
segment++; journal = []; journalBytes = 0; queuedRevision = -1;
|
||||
}
|
||||
journal.push(text + "\n"); journalBytes += bytes; revision++;
|
||||
while (recent.length > 400 || memoryBytes > maxMemoryBytes) {
|
||||
memoryBytes -= recent.shift().bytes; droppedRecords++;
|
||||
}
|
||||
schedule();
|
||||
if (level === "ERROR") void flush();
|
||||
return entry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createReport(entry, rawError) {
|
||||
let known;
|
||||
if (rawError && typeof rawError === "object") known = errors.get(rawError);
|
||||
const fingerprint = `${entry.module}:${entry.event}:${entry.error?.name}:${entry.error?.message}`;
|
||||
if (!known) {
|
||||
const prior = fingerprints.get(fingerprint);
|
||||
if (prior && now() - prior.time < 5000) known = prior;
|
||||
}
|
||||
if (!known) known = { id: `${launchId}-${++reportSequence}`, count: 0,
|
||||
first: entry.time, time: now(), firstFailure: entry };
|
||||
known.count++; known.time = now();
|
||||
if (rawError && typeof rawError === "object") errors.set(rawError, known);
|
||||
fingerprints.set(fingerprint, known);
|
||||
while (fingerprints.size > 64) fingerprints.delete(fingerprints.keys().next().value);
|
||||
entry.reportId = known.id;
|
||||
const file = `report-${known.id}.json`;
|
||||
const report = {
|
||||
schemaVersion: 1, reportId: known.id, firstSeen: known.first,
|
||||
lastSeen: entry.time, occurrences: known.count, environment: metadata,
|
||||
storageAtCapture: snapshot(storageStatus()), firstFailure: known.firstFailure,
|
||||
failure: entry,
|
||||
droppedRecords, droppedWrites,
|
||||
};
|
||||
const records = recent.map(item => {
|
||||
const text = item.text || JSON.stringify(item.entry);
|
||||
return { text, bytes: item.text ? item.bytes - 1 : byteLength(text) };
|
||||
});
|
||||
let start = 0;
|
||||
let bytes = byteLength(JSON.stringify(report)) + 14
|
||||
+ records.reduce((total, item) => total + item.bytes + 1, 0);
|
||||
while (bytes + 32 > maxReportBytes && start < records.length - 1) {
|
||||
bytes -= records[start++].bytes + 1;
|
||||
}
|
||||
report.droppedRecords += start;
|
||||
const text = JSON.stringify(report).slice(0, -1) + ',"records":['
|
||||
+ records.slice(start).map(item => item.text).join(",") + "]}";
|
||||
latestReport = { id: known.id, file, revision: known.count,
|
||||
persisted: false, event: entry.event };
|
||||
enqueue(file, text, true, known.count);
|
||||
}
|
||||
|
||||
function enqueue(name, text, priority = false, reportRevision = null) {
|
||||
if (unavailable || stopped) return;
|
||||
const bytes = byteLength(text);
|
||||
const previous = pending.get(name);
|
||||
if (previous) { pendingBytes -= previous.bytes; pending.delete(name); }
|
||||
if (bytes > maxPendingBytes) { droppedWrites++; return; }
|
||||
while (pending.size >= maxPending || pendingBytes + bytes > maxPendingBytes) {
|
||||
const victim = [...pending].find(([, item]) => !item.priority)?.[0]
|
||||
|| (priority ? pending.keys().next().value : null);
|
||||
if (!victim) { droppedWrites++; return; }
|
||||
pendingBytes -= pending.get(victim).bytes;
|
||||
pending.delete(victim); droppedWrites++;
|
||||
}
|
||||
pending.set(name, { text, bytes, priority, reportRevision });
|
||||
pendingBytes += bytes;
|
||||
startWorker();
|
||||
}
|
||||
|
||||
async function prune(protect, text) {
|
||||
const kind = storageStatus().kind;
|
||||
if (!inventory || inventoryKind !== kind) {
|
||||
inventory = new Map((await storage.list()).filter(file =>
|
||||
isDiagnosticFile(file.name)).map(file => [file.name, file]));
|
||||
inventoryKind = kind;
|
||||
}
|
||||
inventory.set(protect, { name: protect, bytes: byteLength(text), time: now() });
|
||||
const files = [...inventory.values()];
|
||||
files.sort((a, b) => (a.time || 0) - (b.time || 0));
|
||||
let bytes = files.reduce((sum, file) => sum + (file.bytes || 0), 0);
|
||||
let count = files.length;
|
||||
for (const file of files) {
|
||||
if (bytes <= maxDiskBytes && count <= maxFiles) break;
|
||||
if (file.name === protect || file.name === runtimeName()
|
||||
|| file.name === latestReport?.file) continue;
|
||||
await storage.remove(file.name);
|
||||
inventory.delete(file.name);
|
||||
bytes -= file.bytes || 0; count--;
|
||||
}
|
||||
}
|
||||
|
||||
function startWorker() {
|
||||
if (worker || !pending.size) return;
|
||||
worker = Promise.resolve().then(async () => {
|
||||
while (pending.size && !unavailable) {
|
||||
const name = [...pending].find(([, item]) => item.priority)?.[0]
|
||||
|| pending.keys().next().value;
|
||||
const item = pending.get(name);
|
||||
pending.delete(name);
|
||||
pendingBytes -= item.bytes;
|
||||
try {
|
||||
await storage.write(name, item.text);
|
||||
if (latestReport?.file === name && latestReport.revision === item.reportRevision) {
|
||||
latestReport.persisted = true;
|
||||
latestReport.savedStorage = snapshot(storageStatus());
|
||||
}
|
||||
try { await prune(name, item.text); } catch { inventory = null; }
|
||||
} catch {
|
||||
unavailable = true; pending.clear(); pendingBytes = 0;
|
||||
}
|
||||
}
|
||||
}).catch(() => { unavailable = true; pending.clear(); pendingBytes = 0; }).finally(() => {
|
||||
worker = null;
|
||||
if (pending.size && !unavailable) startWorker();
|
||||
});
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
try {
|
||||
clearTimeout(flushTimer); flushTimer = null;
|
||||
if (journal.length && queuedRevision !== revision) {
|
||||
queuedRevision = revision;
|
||||
enqueue(runtimeName(), journal.join(""));
|
||||
}
|
||||
while (worker) await worker;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function scope(module, provider = () => ({})) {
|
||||
const details = extra => {
|
||||
try {
|
||||
const context = typeof provider === "function" ? provider() : provider;
|
||||
return { ...snapshot(context), ...snapshot(extra || {}) };
|
||||
} catch { return { diagnosticContextUnavailable: true }; }
|
||||
};
|
||||
return Object.freeze({
|
||||
info: (event, extra) => record("INFO", event, details(extra), undefined, module),
|
||||
warn: (event, extra, error) => record("WARN", event, details(extra), error, module),
|
||||
error: (event, error, extra) => record("ERROR", event, details(extra), error, module),
|
||||
async step(event, action, extra = {}, options = {}) {
|
||||
const started = monotonic();
|
||||
record("INFO", `${event}.start`, details(extra), undefined, module);
|
||||
let timer;
|
||||
if (options.observeWait !== false) {
|
||||
timer = setTimeout(() => {
|
||||
if (env.document?.hidden) return;
|
||||
record("WARN", `${event}.waiting`, details({ ...extra,
|
||||
elapsedMs: monotonic() - started }), undefined, module);
|
||||
}, options.waitMs ?? 15000);
|
||||
timer?.unref?.();
|
||||
}
|
||||
try {
|
||||
const result = await action();
|
||||
record("INFO", `${event}.complete`, details({ ...extra,
|
||||
elapsedMs: monotonic() - started }), undefined, module);
|
||||
return result;
|
||||
} catch (error) {
|
||||
record("ERROR", `${event}.failed`, details({ ...extra,
|
||||
elapsedMs: monotonic() - started }), error, module);
|
||||
throw error;
|
||||
} finally { clearTimeout(timer); }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function listen(target, event, handler, capture = false) {
|
||||
if (typeof target?.addEventListener !== "function") return;
|
||||
target.addEventListener(event, handler, capture);
|
||||
listeners.push(() => target.removeEventListener(event, handler, capture));
|
||||
}
|
||||
const globalLogger = scope("extension.global");
|
||||
const globalSeen = new WeakMap();
|
||||
function captureGlobal(error, source, extra = {}) {
|
||||
if (!extensionSource(source, roots) && !extensionError(error, roots)) return;
|
||||
if (error && typeof error === "object") {
|
||||
const last = globalSeen.get(error);
|
||||
if (last !== undefined && now() - last < 20) return;
|
||||
globalSeen.set(error, now());
|
||||
}
|
||||
globalLogger.error("uncaught", error, { source, ...extra });
|
||||
}
|
||||
listen(env.window, "error", event => {
|
||||
try {
|
||||
if (extensionSource(event.filename, roots) || extensionError(event.error, roots)) {
|
||||
captureGlobal(event.error || event.message, event.filename,
|
||||
{ line: event.lineno, column: event.colno });
|
||||
} else {
|
||||
const source = event.target?.src || event.target?.href;
|
||||
if (extensionSource(source, roots)) globalLogger.error("resource.load",
|
||||
new Error("术樱包资源加载失败"), { source });
|
||||
}
|
||||
} catch {}
|
||||
}, true);
|
||||
listen(env.window, "unhandledrejection", event => {
|
||||
try {
|
||||
if (extensionError(event.reason, roots)) captureGlobal(event.reason, "",
|
||||
{ origin: "unhandledrejection" });
|
||||
} catch {}
|
||||
});
|
||||
if (env.window) {
|
||||
const previous = env.window.onerror;
|
||||
const chained = function(message, source, line, column, error) {
|
||||
try { captureGlobal(error || message, source, { line, column }); } catch {}
|
||||
return typeof previous === "function" ? previous.apply(this, arguments) : undefined;
|
||||
};
|
||||
env.window.onerror = chained;
|
||||
listeners.push(() => {
|
||||
if (env.window.onerror === chained) env.window.onerror = previous;
|
||||
});
|
||||
}
|
||||
listen(env.document, "visibilitychange", () => {
|
||||
if (env.document.hidden) void flush();
|
||||
});
|
||||
listen(env.window, "pagehide", () => { void flush(); });
|
||||
const api = Object.freeze({
|
||||
...scope("extension"), scope,
|
||||
registerContext(name, provider) {
|
||||
contexts.set(name, provider);
|
||||
return () => contexts.delete(name);
|
||||
},
|
||||
flush,
|
||||
status: () => snapshot({ enabled: true, ...storageStatus(),
|
||||
unavailable, latestReport, droppedRecords, droppedWrites,
|
||||
pendingFiles: pending.size, pendingBytes,
|
||||
runtimeFile: runtimeName(), launchId }),
|
||||
dispose() {
|
||||
void flush(); stopped = true; clearTimeout(flushTimer);
|
||||
for (const remove of listeners) remove();
|
||||
if (current === api) current = inactive;
|
||||
},
|
||||
});
|
||||
api.info("diagnostics.started", { environment: metadata });
|
||||
return api;
|
||||
}
|
||||
|
||||
export function configureDiagnostics({ lib, game, window, document, version, extensionRoot }) {
|
||||
if (current !== inactive) return current;
|
||||
try {
|
||||
current = createDiagnostics({ version, extensionRoot,
|
||||
env: { lib, game, window, document, performance: window?.performance,
|
||||
localStorage: window?.localStorage, indexedDB: window?.indexedDB } });
|
||||
} catch {}
|
||||
return current;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
const secretKey = /password|passwd|secret|token|cookie|authorization|api[_-]?key|credential/i;
|
||||
|
||||
export function cleanText(value, limit = 8192) {
|
||||
let text;
|
||||
try { text = String(value); } catch { return "[unreadable]"; }
|
||||
const truncated = text.length > limit;
|
||||
text = text.slice(0, limit);
|
||||
text = text.replace(/([?&](?:token|key|api[_-]?key|access_token|password|secret|auth)[a-z0-9_-]*=)[^&#\s]*/gi,
|
||||
"$1[redacted]");
|
||||
text = text.replace(/((?:Bearer|Basic)\s+)[\w.+\/=-]+/gi, "$1[redacted]");
|
||||
text = text.replace(/\b(?:https?|ftp):\/\/[^\s/?#]*/gi, (url, offset, source) => {
|
||||
const schemeEnd = url.indexOf("://") + 3;
|
||||
const at = url.lastIndexOf("@");
|
||||
if (at >= schemeEnd) return url.slice(0, schemeEnd) + "[redacted]@" + url.slice(at + 1);
|
||||
if (truncated && offset + url.length === source.length
|
||||
&& url.slice(schemeEnd).includes(":")) return url.slice(0, schemeEnd) + "[redacted]";
|
||||
return url;
|
||||
});
|
||||
return !truncated && text.length <= limit ? text : text.slice(0, limit) + "[truncated]";
|
||||
}
|
||||
|
||||
function read(object, key) {
|
||||
try { return object[key]; } catch { return "[unreadable]"; }
|
||||
}
|
||||
|
||||
export function snapshot(value, depth = 0, seen = new WeakSet()) {
|
||||
if (value === null || value === undefined) return value ?? null;
|
||||
if (typeof value === "string") return cleanText(value, 2048);
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
||||
if (typeof value !== "object") return cleanText(value, 256);
|
||||
if (seen.has(value)) return "[circular]";
|
||||
if (depth >= 5) return "[depth limit]";
|
||||
seen.add(value);
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.slice(0, 32).map(item => snapshot(item, depth + 1, seen));
|
||||
if (value.length > 32) result.push(`[${value.length - 32} more items]`);
|
||||
return result;
|
||||
}
|
||||
let keys;
|
||||
try { keys = Object.keys(value); } catch { return "[unreadable]"; }
|
||||
const result = {};
|
||||
for (const key of keys.slice(0, 40)) {
|
||||
if (key === "__proto__" || key === "constructor") continue;
|
||||
result[key] = secretKey.test(key) ? "[redacted]"
|
||||
: snapshot(read(value, key), depth + 1, seen);
|
||||
}
|
||||
if (keys.length > 40) result.truncatedKeys = keys.length - 40;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function serializeError(error, depth = 0, seen = new WeakSet(),
|
||||
budget = { nodes: 0, characters: 32768 }) {
|
||||
if (++budget.nodes > 32 || budget.characters <= 0) {
|
||||
return { name: "TruncatedError", message: "[error size limit]" };
|
||||
}
|
||||
const text = (value, limit = 8192) => {
|
||||
const result = cleanText(value, Math.max(0, Math.min(limit, budget.characters)));
|
||||
budget.characters -= result.length;
|
||||
return result;
|
||||
};
|
||||
if (!error || typeof error !== "object") {
|
||||
return { name: "ThrownValue", message: text(error) };
|
||||
}
|
||||
if (seen.has(error)) return { name: "CircularError", message: "[circular]" };
|
||||
if (depth >= 5) return { name: "TruncatedError", message: "[depth limit]" };
|
||||
seen.add(error);
|
||||
const result = {
|
||||
name: text(read(error, "name") || "Error", 128),
|
||||
message: text(read(error, "message") || error),
|
||||
stack: text(read(error, "stack") || "", 16384),
|
||||
};
|
||||
for (const key of ["code", "operationUncertain"]) {
|
||||
const value = read(error, key);
|
||||
if (value !== undefined) result[key] = snapshot(value);
|
||||
}
|
||||
const cause = read(error, "cause");
|
||||
if (cause !== undefined) result.cause = serializeError(cause, depth + 1, seen, budget);
|
||||
const errors = read(error, "errors");
|
||||
if (Array.isArray(errors)) {
|
||||
result.errors = errors.slice(0, 16).map(item =>
|
||||
serializeError(item, depth + 1, seen, budget));
|
||||
if (errors.length > 16) result.truncatedErrors = errors.length - 16;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function decode(value) {
|
||||
let text = cleanText(value, 32768).replace(/\\/g, "/");
|
||||
for (let count = 0; count < 2; count++) {
|
||||
try { text = decodeURIComponent(text); } catch { break; }
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export function extensionSource(value, roots = []) {
|
||||
if (typeof value !== "string" || !value) return false;
|
||||
const source = cleanText(value, 32768).replace(/\\/g, "/").trim();
|
||||
const url = source.match(/(?:file|https?):\/\/[^\n]+/i)?.[0];
|
||||
const native = source.match(/[a-z]:\/[^\n]+/i)?.[0];
|
||||
const candidate = decode((url || native || source)
|
||||
.replace(/\)\s*$/, "").split(/[?#]/, 1)[0]);
|
||||
if (roots.some(root => {
|
||||
const base = decode(root).split(/[?#]/, 1)[0];
|
||||
return base && (candidate.startsWith(base)
|
||||
|| (base.startsWith("file:") && candidate.startsWith(base.replace(/^file:\/+/, ""))));
|
||||
})) return true;
|
||||
if (/^(?:\.?\/)?extension\/术樱包\//.test(candidate)) return true;
|
||||
return !roots.length && /\/extension\/术樱包\//.test(candidate);
|
||||
}
|
||||
|
||||
export function extensionError(error, roots = [], depth = 0) {
|
||||
if (!error || typeof error !== "object" || depth >= 5) return false;
|
||||
const stack = read(error, "stack");
|
||||
if (typeof stack === "string" && stack.split("\n").slice(1).some(line =>
|
||||
/(?:\bat\s|@)/.test(line) && extensionSource(line, roots))) return true;
|
||||
if (extensionSource(read(error, "fileName"), roots)) return true;
|
||||
if (extensionError(read(error, "cause"), roots, depth + 1)) return true;
|
||||
const errors = read(error, "errors");
|
||||
return Array.isArray(errors) && errors.slice(0, 16).some(item =>
|
||||
extensionError(item, roots, depth + 1));
|
||||
}
|
||||
|
||||
export function byteLength(text) {
|
||||
if (typeof TextEncoder === "function") return new TextEncoder().encode(text).length;
|
||||
return new Blob([text]).size;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { byteLength } from "./serialize.js";
|
||||
|
||||
export const LOG_DIRECTORY = "extension/术樱包/logs";
|
||||
const ownedName = /^(?:runtime|report)-[a-zA-Z0-9-]+\.(?:jsonl|json)$/;
|
||||
|
||||
export function isDiagnosticFile(name) {
|
||||
return typeof name === "string" && ownedName.test(name);
|
||||
}
|
||||
|
||||
function checkedName(name) {
|
||||
if (!isDiagnosticFile(name)) throw new Error("Invalid diagnostic filename");
|
||||
return name;
|
||||
}
|
||||
|
||||
function callback(start) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try { start(resolve, reject); } catch (error) { reject(error); }
|
||||
});
|
||||
}
|
||||
|
||||
function nodeStorage(env) {
|
||||
const fs = env.lib?.node?.fs;
|
||||
const root = env.window?.__dirname;
|
||||
if (!fs || typeof root !== "string" || !root) return null;
|
||||
const directory = `${root.replace(/[\\/]$/, "")}/${LOG_DIRECTORY}`;
|
||||
const invoke = (method, ...args) => callback((resolve, reject) => {
|
||||
fs[method](...args, (error, value) => error ? reject(error) : resolve(value));
|
||||
});
|
||||
const ready = () => invoke("mkdir", directory, { recursive: true });
|
||||
return {
|
||||
kind: "file", location: directory,
|
||||
async write(name, text) {
|
||||
await ready();
|
||||
await invoke("writeFile", `${directory}/${checkedName(name)}`, text, "utf8");
|
||||
},
|
||||
async list() {
|
||||
await ready();
|
||||
const names = (await invoke("readdir", directory)).filter(isDiagnosticFile);
|
||||
const entries = [];
|
||||
for (const name of names) {
|
||||
const stat = await invoke("lstat", `${directory}/${name}`);
|
||||
if (stat.isFile()) entries.push({ name, bytes: stat.size, time: stat.mtimeMs });
|
||||
}
|
||||
return entries;
|
||||
},
|
||||
remove: name => invoke("unlink", `${directory}/${checkedName(name)}`),
|
||||
};
|
||||
}
|
||||
|
||||
function cordovaStorage(env) {
|
||||
const host = env.window;
|
||||
if (typeof host?.resolveLocalFileSystemURL !== "function") return null;
|
||||
const root = env.localStorage?.getItem("noname_inited");
|
||||
if (!root) return null;
|
||||
let directoryPromise;
|
||||
const directory = () => {
|
||||
if (!directoryPromise) directoryPromise = callback((resolve, reject) => {
|
||||
host.resolveLocalFileSystemURL(root, resolve, reject);
|
||||
}).then(async entry => {
|
||||
for (const part of LOG_DIRECTORY.split("/")) {
|
||||
entry = await callback((resolve, reject) =>
|
||||
entry.getDirectory(part, { create: true }, resolve, reject));
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
return directoryPromise;
|
||||
};
|
||||
const file = async name => {
|
||||
const entry = await directory();
|
||||
return callback((resolve, reject) =>
|
||||
entry.getFile(checkedName(name), { create: true }, resolve, reject));
|
||||
};
|
||||
return {
|
||||
kind: "file", location: `${root.replace(/\/$/, "")}/${LOG_DIRECTORY}`,
|
||||
async write(name, text) {
|
||||
const entry = await file(name);
|
||||
await callback((resolve, reject) => entry.createWriter(writer => {
|
||||
let truncated = false;
|
||||
writer.onerror = () => reject(writer.error || new Error("Diagnostic write failed"));
|
||||
writer.onwriteend = () => {
|
||||
if (writer.error) { reject(writer.error); return; }
|
||||
if (truncated) { resolve(); return; }
|
||||
truncated = true;
|
||||
try { writer.write(new Blob([text], { type: "text/plain;charset=utf-8" })); }
|
||||
catch (error) { reject(error); }
|
||||
};
|
||||
try { writer.truncate(0); } catch (error) { reject(error); }
|
||||
}, reject));
|
||||
},
|
||||
async list() {
|
||||
const entry = await directory();
|
||||
const reader = entry.createReader();
|
||||
const entries = [];
|
||||
while (true) {
|
||||
const batch = await callback((resolve, reject) =>
|
||||
reader.readEntries(resolve, reject));
|
||||
if (!batch.length) break;
|
||||
for (const item of batch) {
|
||||
if (!item.isFile || !isDiagnosticFile(item.name)) continue;
|
||||
const metadata = await callback((resolve, reject) =>
|
||||
item.getMetadata(resolve, reject));
|
||||
entries.push({ name: item.name, bytes: metadata.size,
|
||||
time: new Date(metadata.modificationTime).getTime() });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
},
|
||||
async remove(name) {
|
||||
const entry = await file(name);
|
||||
return callback((resolve, reject) => entry.remove(resolve, reject));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function databaseStorage(env) {
|
||||
const indexedDB = env.indexedDB;
|
||||
if (!indexedDB) return null;
|
||||
let database;
|
||||
const open = () => database ||= callback((resolve, reject) => {
|
||||
const request = indexedDB.open("shuying-diagnostics", 1);
|
||||
let failed = false;
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains("files")) db.createObjectStore("files", { keyPath: "name" });
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
if (failed) { request.result.close(); return; }
|
||||
request.result.onversionchange = () => request.result.close();
|
||||
resolve(request.result);
|
||||
};
|
||||
request.onerror = () => { failed = true; reject(request.error); };
|
||||
request.onblocked = () => { failed = true; reject(new Error("Diagnostic database blocked")); };
|
||||
});
|
||||
const transaction = async action => {
|
||||
const db = await open();
|
||||
return callback((resolve, reject) => {
|
||||
const tx = db.transaction(["files"], "readwrite");
|
||||
let value, failure;
|
||||
tx.oncomplete = () => resolve(value);
|
||||
tx.onerror = event => { failure = event.target?.error; };
|
||||
tx.onabort = () => reject(tx.error || failure || new Error("Diagnostic transaction aborted"));
|
||||
try { action(tx.objectStore("files"), result => { value = result; }); }
|
||||
catch (error) { try { tx.abort(); } catch {} reject(error); }
|
||||
});
|
||||
};
|
||||
return {
|
||||
kind: "indexeddb", location: "本机独立诊断数据库",
|
||||
write(name, text) {
|
||||
return transaction(store => store.put({ name: checkedName(name), text,
|
||||
bytes: byteLength(text), time: Date.now() }));
|
||||
},
|
||||
list() {
|
||||
return transaction((store, done) => {
|
||||
const entries = [];
|
||||
const cursor = store.openCursor();
|
||||
cursor.onsuccess = () => {
|
||||
const item = cursor.result;
|
||||
if (!item) { done(entries); return; }
|
||||
if (isDiagnosticFile(item.value.name)) entries.push({
|
||||
name: item.value.name, bytes: item.value.bytes, time: item.value.time,
|
||||
});
|
||||
item.continue();
|
||||
};
|
||||
});
|
||||
},
|
||||
remove: name => transaction(store => store.delete(checkedName(name))),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDiagnosticStorage(env, options = {}) {
|
||||
const timeoutMs = options.timeoutMs ?? 5000;
|
||||
const onFailure = options.onFailure || (() => {});
|
||||
const candidates = [];
|
||||
try {
|
||||
const native = nodeStorage(env) || cordovaStorage(env);
|
||||
if (native) candidates.push(native);
|
||||
} catch (error) { onFailure(error, "file.init"); }
|
||||
try {
|
||||
const database = databaseStorage(env);
|
||||
if (database) candidates.push(database);
|
||||
} catch (error) { onFailure(error, "database.init"); }
|
||||
let index = 0;
|
||||
const timed = action => new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("Diagnostic storage timed out")), timeoutMs);
|
||||
Promise.resolve().then(action).then(resolve, reject).finally(() => clearTimeout(timer));
|
||||
});
|
||||
const execute = async (method, ...args) => {
|
||||
while (index < candidates.length) {
|
||||
const backend = candidates[index];
|
||||
try { return await timed(() => backend[method](...args)); }
|
||||
catch (error) {
|
||||
try { onFailure(error, `${backend.kind}.${method}`); } catch {}
|
||||
if (method !== "write") throw error;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
throw new Error("Diagnostic persistence unavailable; records kept in memory");
|
||||
};
|
||||
return {
|
||||
write: (name, text) => execute("write", name, text),
|
||||
list: () => execute("list"),
|
||||
remove: name => execute("remove", name),
|
||||
status: () => ({ kind: candidates[index]?.kind || "memory",
|
||||
location: candidates[index]?.location || "仅当前运行内存,未能持久化" }),
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"version": "2.0.1.1-rc.1",
|
||||
"generated_at": "2026-10-04T11:24:22.012885+00:00",
|
||||
"version": "2.0.2.3-rc.3",
|
||||
"generated_at": "2026-10-08T15:53:16.835760+00:00",
|
||||
"algorithm": "sha256",
|
||||
"update_channels": true,
|
||||
"file_count": 278,
|
||||
"size_bytes": 55481243,
|
||||
"folder_sha256": "4e8e4d9e1f1f51c4687aa26b055887479a5ea48d6bfac1a149a3ed10259e8c49",
|
||||
"file_count": 281,
|
||||
"size_bytes": 47971941,
|
||||
"folder_sha256": "8211bd35c6c1ce35b7e4ce7f36016d7afa8b3d4c51dd64d5ff47c5cabee72f8a",
|
||||
"core": [
|
||||
"extension.js",
|
||||
"menu.js",
|
||||
"function.js",
|
||||
"update.js",
|
||||
"diagnostics/index.js",
|
||||
"diagnostics/serialize.js",
|
||||
"diagnostics/storage.js",
|
||||
"tianshu/extension.js",
|
||||
"tianshu/config.js",
|
||||
"pve/boss/index.js",
|
||||
@@ -292,8 +295,8 @@
|
||||
"removeDirectories": [],
|
||||
"files": {
|
||||
"LICENSE": {
|
||||
"size": 35147,
|
||||
"sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903"
|
||||
"size": 35821,
|
||||
"sha256": "0b383d5a63da644f628d99c33976ea6487ed89aaa59f0b3257992deac1171e6b"
|
||||
},
|
||||
"character/audio/ganning/fenwei_shuying1.mp3": {
|
||||
"size": 83302,
|
||||
@@ -344,20 +347,20 @@
|
||||
"sha256": "f330d2665fd2c9bb85cd4a80f596f44a283c715333a1bf8aafffa5ef030c5126"
|
||||
},
|
||||
"character/character.js": {
|
||||
"size": 653,
|
||||
"sha256": "6cbd7ec49ec9b9ec362f5dadb4251c219ecd468a099a082f17da86c61e0823f2"
|
||||
"size": 673,
|
||||
"sha256": "e90a755479cce77fa083926f221065bae3e42fa6c347537fe05a5c4d00fef72f"
|
||||
},
|
||||
"character/characterFilter.js": {
|
||||
"size": 63,
|
||||
"sha256": "92164cec19b58e3a27d92426fbe227c426dff1391b2f56b2329a676a5fc4faf7"
|
||||
"size": 66,
|
||||
"sha256": "0dc51d476375435b50ea965fd540a6afb19134d974fe183bd924225dc3842062"
|
||||
},
|
||||
"character/characterTitle.js": {
|
||||
"size": 61,
|
||||
"sha256": "f4be860d37d46261a6d45e30bce661d301d79d548aeef0bfc2e8ad67fe783cea"
|
||||
"size": 64,
|
||||
"sha256": "b736420c47616f5e6493bc0e5d00a1a6803e8540e312ff99b0a5ae2d2de17b6f"
|
||||
},
|
||||
"character/dynamicTranslate.js": {
|
||||
"size": 872,
|
||||
"sha256": "63a56c336e881b15e08374656863b67b7465eedf6af683f8bade3d33b12f2704"
|
||||
"size": 883,
|
||||
"sha256": "58b234c5f801b6d7ec65ea5344d4cb075f9b2e1d2f4df65ef95ea24b3ddba995"
|
||||
},
|
||||
"character/images/ganning_shuying.jpg": {
|
||||
"size": 1580990,
|
||||
@@ -368,48 +371,60 @@
|
||||
"sha256": "ff13df281bd289d63033d2c52b8d31f2a9f0a8d9cdc0ad525db06b899411f4cb"
|
||||
},
|
||||
"character/index.js": {
|
||||
"size": 1002,
|
||||
"sha256": "ef3dcf49147bead5a4b58c56aa23f983e7fda7c4769f276f1f228293f87acc33"
|
||||
"size": 1030,
|
||||
"sha256": "3e365b2c30eb0f9a890f7b4ed777f337a0ea4a202e7db0132e4b533a5ad5bbb0"
|
||||
},
|
||||
"character/intro.js": {
|
||||
"size": 61,
|
||||
"sha256": "68187ccda36e322c0483a735e6de6e04e5d7ded9e6f6aa9a3e931f4400db2a5e"
|
||||
"size": 64,
|
||||
"sha256": "17fc80689618ad9d3601cc9ccb692e38eb7b66de82e21c75c246c749f2bdf1fd"
|
||||
},
|
||||
"character/perfectPairs.js": {
|
||||
"size": 55,
|
||||
"sha256": "7f2bd697eaaff284d2b356321a402a3cc7c0139ff6fdf339f6cbdd8da208a05f"
|
||||
"size": 58,
|
||||
"sha256": "a9b5f18910cf92cd6a727cf254ad8d6e1e2ba60a9cf35e9a458da34ce4fda49f"
|
||||
},
|
||||
"character/skill.js": {
|
||||
"size": 31285,
|
||||
"sha256": "608ad1b6463bda8318b91f729dce0cdf98a223d0a72fd1fac35cf32b49e69c08"
|
||||
"size": 32111,
|
||||
"sha256": "78294951c6d0e53ab2ad718aa128d437e3eea2579bb9644546a79864875e21f6"
|
||||
},
|
||||
"character/sort.js": {
|
||||
"size": 112,
|
||||
"sha256": "42cf6bb834aca4ff883aa196604f3d9d2d9047bb320cf56a633aae4ecbb5ce4c"
|
||||
"size": 116,
|
||||
"sha256": "a2864d98878342280c81bc527a1168b4dccf7976dc40e49c50c9037679bc7ca5"
|
||||
},
|
||||
"character/translate.js": {
|
||||
"size": 2091,
|
||||
"sha256": "2a31d41939f1d636f9c1b2378d0f15cf7aa5c9e2caa35b465066deb4f877e4bb"
|
||||
"size": 2108,
|
||||
"sha256": "e533d53409068342100dbdb42ec0832aac656988ca42a168a915dd851e7b4d06"
|
||||
},
|
||||
"character/voices.js": {
|
||||
"size": 43,
|
||||
"sha256": "d7f3da55f8b2e8bf77ace03bd4911497a4abb9e1f942555f7685337801278205"
|
||||
"size": 46,
|
||||
"sha256": "54787af1d4efcd35f20c5eca55db2e60f0cd160d7fed728440542f118e7ae44a"
|
||||
},
|
||||
"diagnostics/index.js": {
|
||||
"size": 14862,
|
||||
"sha256": "bb89045b70dd36ba3539b74ea80725e57f28bfae4d332c04c1e8319d99258bd6"
|
||||
},
|
||||
"diagnostics/serialize.js": {
|
||||
"size": 5607,
|
||||
"sha256": "1456abb806511dcbe2f3cfea5f036741d8c0761a7a6c1f008a73c52007d1086d"
|
||||
},
|
||||
"diagnostics/storage.js": {
|
||||
"size": 8047,
|
||||
"sha256": "3d6b861ce1186cd7d8123b61e7e8095f36bc2af041b13ab3511b04bb0b375f86"
|
||||
},
|
||||
"extension.css": {
|
||||
"size": 51738,
|
||||
"sha256": "356aa9473ab72ad43850c13451fd15a25c6ce2d93678cd0dcba5568d10443b40"
|
||||
"size": 57424,
|
||||
"sha256": "f62d07820aa0f93915719bdd334b3513673b0b02d94e89f12deed3c07c4731dd"
|
||||
},
|
||||
"extension.js": {
|
||||
"size": 15464,
|
||||
"sha256": "f59c38efab9c337b7f8938e1f71034a2e58ea8b5b00470f5ef2ec61e26a9b5e4"
|
||||
"size": 16317,
|
||||
"sha256": "eee5e5ec1ba9f34a613d14812f1829c8aec656080812533596405434551983f6"
|
||||
},
|
||||
"function.js": {
|
||||
"size": 33746,
|
||||
"sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909"
|
||||
"size": 36629,
|
||||
"sha256": "33ba3335fee271173fc454b25c5122e9e2b686eabdff30a54599e5cd31800710"
|
||||
},
|
||||
"menu.js": {
|
||||
"size": 25104,
|
||||
"sha256": "d894ef33889ecf1233e1107395456985da4e71a9f679a2c3a493c7c2fa57f8d7"
|
||||
"size": 30671,
|
||||
"sha256": "3b6af8ff6d53c289f94fbaa320be91c3a7a0b554ba999e0bfc26ae6227981862"
|
||||
},
|
||||
"pve/audio/dead/十二生肖/chenlong.mp3": {
|
||||
"size": 68757,
|
||||
@@ -1012,8 +1027,8 @@
|
||||
"sha256": "baa234a4f9b902e647f0df3a5189ac65312332fa20c998727c855d9afad0c56c"
|
||||
},
|
||||
"pve/boss/sets/tianshu/voices.js": {
|
||||
"size": 609,
|
||||
"sha256": "020d42f5a8ab6cfe2189de4f8b27aea2f5eb8f0c92eabeccc30a69311fc684bb"
|
||||
"size": 615,
|
||||
"sha256": "0c36f009f74fff163dda7865a3c3e493a2da73c39d44c4873bc1eadd24c3a6b0"
|
||||
},
|
||||
"pve/boss/sets/zhuoguiquxie/card.js": {
|
||||
"size": 126,
|
||||
@@ -1052,24 +1067,24 @@
|
||||
"sha256": "4a8648524425c2edfb8595971476398ff3c513d27ff4850895c0839945b5c1c6"
|
||||
},
|
||||
"pve/boss/sets/zhuoguiquxie/skill.js": {
|
||||
"size": 46766,
|
||||
"sha256": "406b8bc5e8db6010650e7e4674c5a39f54e27374e4ae358d988f28a2b6e04629"
|
||||
"size": 46830,
|
||||
"sha256": "bb729ce909a0e2c3d11c0833741eaf6493e6e13c2689926ae444f2d55dd24bb0"
|
||||
},
|
||||
"pve/boss/sets/zhuoguiquxie/sort.js": {
|
||||
"size": 390,
|
||||
"sha256": "3855904cdc28e6301b9f9177b6c8e04d943ef44fd002a2ce83de5aab7572930c"
|
||||
},
|
||||
"pve/boss/sets/zhuoguiquxie/translate.js": {
|
||||
"size": 6820,
|
||||
"sha256": "9f20e9429faab842c5a2c3e94d94595cc2480e78752fff3e7f5ac54d09161c1b"
|
||||
"size": 6823,
|
||||
"sha256": "a0dde570b580beb633432ee1d74c2051520d7e5829c584297fec0c87e62c2684"
|
||||
},
|
||||
"pve/boss/sets/zhuoguiquxie/voices.js": {
|
||||
"size": 2115,
|
||||
"sha256": "d93f873a87649d413d1fc7b827c8bdb01e5f61cc11d9650f64b59de9d575e9e1"
|
||||
},
|
||||
"pve/boss/shared.js": {
|
||||
"size": 416,
|
||||
"sha256": "9c0c7b0757172078f920a91cc27569456c2d1e3134130391c88ec5ab86370f79"
|
||||
"size": 425,
|
||||
"sha256": "61bef51415590e0d37ea7d08f5b34306fd1dd3c7591cd8de77dc7ed629c49b98"
|
||||
},
|
||||
"pve/boss/skill.js": {
|
||||
"size": 271,
|
||||
@@ -1084,8 +1099,8 @@
|
||||
"sha256": "f58cf928b22b712fcb374f19e5fd3343d4035e2702388b0d09b11abdd1499376"
|
||||
},
|
||||
"pve/boss/voices.js": {
|
||||
"size": 682,
|
||||
"sha256": "a72b8a89b18f393665e773678630422bb8dd115abc6cb668374a7057be8659cb"
|
||||
"size": 700,
|
||||
"sha256": "08e50d4bc547950b939949f2d1f12bf08aefbc65e84f26cddd76b33b242f11a0"
|
||||
},
|
||||
"pve/images/tianshu.jpg": {
|
||||
"size": 250592,
|
||||
@@ -1200,8 +1215,8 @@
|
||||
"sha256": "6787f2d322dcb02edc41bb282388623ac6d1fe9bd5966ba6fe017a7cb2bbe929"
|
||||
},
|
||||
"pve/images/青青子衿/simayi_boss_shuying.jpg": {
|
||||
"size": 7770307,
|
||||
"sha256": "4752a33861a9ed55633de356e60819a73918cc99c82f3c0544a0befc29515473"
|
||||
"size": 195590,
|
||||
"sha256": "26933ced3153897bb9cc7f25f2a8098e0d376235c538946e1a22e6b91b4d1ddc"
|
||||
},
|
||||
"pve/images/青青子衿/yuanshu_boss_shuying.jpg": {
|
||||
"size": 2155807,
|
||||
@@ -1284,32 +1299,32 @@
|
||||
"sha256": "15adcfb80bd22035c604703005ae22d1721eeafcc0a1ee09c89c73713cd3d03a"
|
||||
},
|
||||
"tianshu/bondSkills.js": {
|
||||
"size": 41588,
|
||||
"sha256": "0b5a1ec26d8a8881057aa1fc8d95fecaf06fa6753d23c99a4978de83f8c44ec6"
|
||||
"size": 42902,
|
||||
"sha256": "cd0a38257095269f5cee24e246d7c5961b849026375fab6f9754468dd9df329c"
|
||||
},
|
||||
"tianshu/config.js": {
|
||||
"size": 29707,
|
||||
"sha256": "9b331adca63ff4b43dc06f707c3bd728d3ce0269b1c1f4cedec5e42281f13ff0"
|
||||
},
|
||||
"tianshu/extension.js": {
|
||||
"size": 76830,
|
||||
"sha256": "ae2585a6425cbcdf7cd2dee0f1151f5112fb8cfd4e8f6e089c6827774d6dbf9b"
|
||||
"size": 77106,
|
||||
"sha256": "02e3cdd9b05b7912ee0454b9aff918576e128c1294b64ea722dcd3d2c446a872"
|
||||
},
|
||||
"tianshu/shuYing_Skill_Xvxiang1.mp3": {
|
||||
"size": 15451,
|
||||
"sha256": "41914d7d542ddfb93c89aefffc245869d63a9fb2f52293df8e6667009832afd8"
|
||||
},
|
||||
"update.js": {
|
||||
"size": 60005,
|
||||
"sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7"
|
||||
"size": 77689,
|
||||
"sha256": "07b1a8e332fdb996e47455b34106d09d749701bf3cd26dd843d016edc03ae266"
|
||||
},
|
||||
"update.txt": {
|
||||
"size": 3953,
|
||||
"sha256": "a81636c5b7c6dcf67b2e7b88b87668f3bb9626d974249c6e5413015cd1d13a21"
|
||||
"size": 4849,
|
||||
"sha256": "605be3429a648ecd676c85ac3e42598be129ab6a19d93b77fc34d7735e090f93"
|
||||
},
|
||||
"voices.js": {
|
||||
"size": 1711,
|
||||
"sha256": "975c58fe201cbbc0027af5038504064382a195df278085a75eca03b2ddace75a"
|
||||
"size": 1756,
|
||||
"sha256": "e25930cca7ea95af9337d01a098795c1e14dd2c9628baaeb0b7c7c140fca9c9f"
|
||||
},
|
||||
"yuanshen/extension.js": {
|
||||
"size": 1082,
|
||||
@@ -1406,12 +1421,12 @@
|
||||
},
|
||||
"tree": {
|
||||
"type": "directory",
|
||||
"name": "术樱包",
|
||||
"name": "source",
|
||||
"children": {
|
||||
"LICENSE": {
|
||||
"type": "file",
|
||||
"size": 35147,
|
||||
"sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903"
|
||||
"size": 35821,
|
||||
"sha256": "0b383d5a63da644f628d99c33976ea6487ed89aaa59f0b3257992deac1171e6b"
|
||||
},
|
||||
"character": {
|
||||
"type": "directory",
|
||||
@@ -1493,23 +1508,23 @@
|
||||
},
|
||||
"character.js": {
|
||||
"type": "file",
|
||||
"size": 653,
|
||||
"sha256": "6cbd7ec49ec9b9ec362f5dadb4251c219ecd468a099a082f17da86c61e0823f2"
|
||||
"size": 673,
|
||||
"sha256": "e90a755479cce77fa083926f221065bae3e42fa6c347537fe05a5c4d00fef72f"
|
||||
},
|
||||
"characterFilter.js": {
|
||||
"type": "file",
|
||||
"size": 63,
|
||||
"sha256": "92164cec19b58e3a27d92426fbe227c426dff1391b2f56b2329a676a5fc4faf7"
|
||||
"size": 66,
|
||||
"sha256": "0dc51d476375435b50ea965fd540a6afb19134d974fe183bd924225dc3842062"
|
||||
},
|
||||
"characterTitle.js": {
|
||||
"type": "file",
|
||||
"size": 61,
|
||||
"sha256": "f4be860d37d46261a6d45e30bce661d301d79d548aeef0bfc2e8ad67fe783cea"
|
||||
"size": 64,
|
||||
"sha256": "b736420c47616f5e6493bc0e5d00a1a6803e8540e312ff99b0a5ae2d2de17b6f"
|
||||
},
|
||||
"dynamicTranslate.js": {
|
||||
"type": "file",
|
||||
"size": 872,
|
||||
"sha256": "63a56c336e881b15e08374656863b67b7465eedf6af683f8bade3d33b12f2704"
|
||||
"size": 883,
|
||||
"sha256": "58b234c5f801b6d7ec65ea5344d4cb075f9b2e1d2f4df65ef95ea24b3ddba995"
|
||||
},
|
||||
"images": {
|
||||
"type": "directory",
|
||||
@@ -1528,60 +1543,80 @@
|
||||
},
|
||||
"index.js": {
|
||||
"type": "file",
|
||||
"size": 1002,
|
||||
"sha256": "ef3dcf49147bead5a4b58c56aa23f983e7fda7c4769f276f1f228293f87acc33"
|
||||
"size": 1030,
|
||||
"sha256": "3e365b2c30eb0f9a890f7b4ed777f337a0ea4a202e7db0132e4b533a5ad5bbb0"
|
||||
},
|
||||
"intro.js": {
|
||||
"type": "file",
|
||||
"size": 61,
|
||||
"sha256": "68187ccda36e322c0483a735e6de6e04e5d7ded9e6f6aa9a3e931f4400db2a5e"
|
||||
"size": 64,
|
||||
"sha256": "17fc80689618ad9d3601cc9ccb692e38eb7b66de82e21c75c246c749f2bdf1fd"
|
||||
},
|
||||
"perfectPairs.js": {
|
||||
"type": "file",
|
||||
"size": 55,
|
||||
"sha256": "7f2bd697eaaff284d2b356321a402a3cc7c0139ff6fdf339f6cbdd8da208a05f"
|
||||
"size": 58,
|
||||
"sha256": "a9b5f18910cf92cd6a727cf254ad8d6e1e2ba60a9cf35e9a458da34ce4fda49f"
|
||||
},
|
||||
"skill.js": {
|
||||
"type": "file",
|
||||
"size": 31285,
|
||||
"sha256": "608ad1b6463bda8318b91f729dce0cdf98a223d0a72fd1fac35cf32b49e69c08"
|
||||
"size": 32111,
|
||||
"sha256": "78294951c6d0e53ab2ad718aa128d437e3eea2579bb9644546a79864875e21f6"
|
||||
},
|
||||
"sort.js": {
|
||||
"type": "file",
|
||||
"size": 112,
|
||||
"sha256": "42cf6bb834aca4ff883aa196604f3d9d2d9047bb320cf56a633aae4ecbb5ce4c"
|
||||
"size": 116,
|
||||
"sha256": "a2864d98878342280c81bc527a1168b4dccf7976dc40e49c50c9037679bc7ca5"
|
||||
},
|
||||
"translate.js": {
|
||||
"type": "file",
|
||||
"size": 2091,
|
||||
"sha256": "2a31d41939f1d636f9c1b2378d0f15cf7aa5c9e2caa35b465066deb4f877e4bb"
|
||||
"size": 2108,
|
||||
"sha256": "e533d53409068342100dbdb42ec0832aac656988ca42a168a915dd851e7b4d06"
|
||||
},
|
||||
"voices.js": {
|
||||
"type": "file",
|
||||
"size": 43,
|
||||
"sha256": "d7f3da55f8b2e8bf77ace03bd4911497a4abb9e1f942555f7685337801278205"
|
||||
"size": 46,
|
||||
"sha256": "54787af1d4efcd35f20c5eca55db2e60f0cd160d7fed728440542f118e7ae44a"
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"type": "directory",
|
||||
"children": {
|
||||
"index.js": {
|
||||
"type": "file",
|
||||
"size": 14862,
|
||||
"sha256": "bb89045b70dd36ba3539b74ea80725e57f28bfae4d332c04c1e8319d99258bd6"
|
||||
},
|
||||
"serialize.js": {
|
||||
"type": "file",
|
||||
"size": 5607,
|
||||
"sha256": "1456abb806511dcbe2f3cfea5f036741d8c0761a7a6c1f008a73c52007d1086d"
|
||||
},
|
||||
"storage.js": {
|
||||
"type": "file",
|
||||
"size": 8047,
|
||||
"sha256": "3d6b861ce1186cd7d8123b61e7e8095f36bc2af041b13ab3511b04bb0b375f86"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extension.css": {
|
||||
"type": "file",
|
||||
"size": 51738,
|
||||
"sha256": "356aa9473ab72ad43850c13451fd15a25c6ce2d93678cd0dcba5568d10443b40"
|
||||
"size": 57424,
|
||||
"sha256": "f62d07820aa0f93915719bdd334b3513673b0b02d94e89f12deed3c07c4731dd"
|
||||
},
|
||||
"extension.js": {
|
||||
"type": "file",
|
||||
"size": 15464,
|
||||
"sha256": "f59c38efab9c337b7f8938e1f71034a2e58ea8b5b00470f5ef2ec61e26a9b5e4"
|
||||
"size": 16317,
|
||||
"sha256": "eee5e5ec1ba9f34a613d14812f1829c8aec656080812533596405434551983f6"
|
||||
},
|
||||
"function.js": {
|
||||
"type": "file",
|
||||
"size": 33746,
|
||||
"sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909"
|
||||
"size": 36629,
|
||||
"sha256": "33ba3335fee271173fc454b25c5122e9e2b686eabdff30a54599e5cd31800710"
|
||||
},
|
||||
"menu.js": {
|
||||
"type": "file",
|
||||
"size": 25104,
|
||||
"sha256": "d894ef33889ecf1233e1107395456985da4e71a9f679a2c3a493c7c2fa57f8d7"
|
||||
"size": 30671,
|
||||
"sha256": "3b6af8ff6d53c289f94fbaa320be91c3a7a0b554ba999e0bfc26ae6227981862"
|
||||
},
|
||||
"pve": {
|
||||
"type": "directory",
|
||||
@@ -2412,8 +2447,8 @@
|
||||
},
|
||||
"voices.js": {
|
||||
"type": "file",
|
||||
"size": 609,
|
||||
"sha256": "020d42f5a8ab6cfe2189de4f8b27aea2f5eb8f0c92eabeccc30a69311fc684bb"
|
||||
"size": 615,
|
||||
"sha256": "0c36f009f74fff163dda7865a3c3e493a2da73c39d44c4873bc1eadd24c3a6b0"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2467,8 +2502,8 @@
|
||||
},
|
||||
"skill.js": {
|
||||
"type": "file",
|
||||
"size": 46766,
|
||||
"sha256": "406b8bc5e8db6010650e7e4674c5a39f54e27374e4ae358d988f28a2b6e04629"
|
||||
"size": 46830,
|
||||
"sha256": "bb729ce909a0e2c3d11c0833741eaf6493e6e13c2689926ae444f2d55dd24bb0"
|
||||
},
|
||||
"sort.js": {
|
||||
"type": "file",
|
||||
@@ -2477,8 +2512,8 @@
|
||||
},
|
||||
"translate.js": {
|
||||
"type": "file",
|
||||
"size": 6820,
|
||||
"sha256": "9f20e9429faab842c5a2c3e94d94595cc2480e78752fff3e7f5ac54d09161c1b"
|
||||
"size": 6823,
|
||||
"sha256": "a0dde570b580beb633432ee1d74c2051520d7e5829c584297fec0c87e62c2684"
|
||||
},
|
||||
"voices.js": {
|
||||
"type": "file",
|
||||
@@ -2491,8 +2526,8 @@
|
||||
},
|
||||
"shared.js": {
|
||||
"type": "file",
|
||||
"size": 416,
|
||||
"sha256": "9c0c7b0757172078f920a91cc27569456c2d1e3134130391c88ec5ab86370f79"
|
||||
"size": 425,
|
||||
"sha256": "61bef51415590e0d37ea7d08f5b34306fd1dd3c7591cd8de77dc7ed629c49b98"
|
||||
},
|
||||
"skill.js": {
|
||||
"type": "file",
|
||||
@@ -2511,8 +2546,8 @@
|
||||
},
|
||||
"voices.js": {
|
||||
"type": "file",
|
||||
"size": 682,
|
||||
"sha256": "a72b8a89b18f393665e773678630422bb8dd115abc6cb668374a7057be8659cb"
|
||||
"size": 700,
|
||||
"sha256": "08e50d4bc547950b939949f2d1f12bf08aefbc65e84f26cddd76b33b242f11a0"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2679,8 +2714,8 @@
|
||||
},
|
||||
"simayi_boss_shuying.jpg": {
|
||||
"type": "file",
|
||||
"size": 7770307,
|
||||
"sha256": "4752a33861a9ed55633de356e60819a73918cc99c82f3c0544a0befc29515473"
|
||||
"size": 195590,
|
||||
"sha256": "26933ced3153897bb9cc7f25f2a8098e0d376235c538946e1a22e6b91b4d1ddc"
|
||||
},
|
||||
"yuanshu_boss_shuying.jpg": {
|
||||
"type": "file",
|
||||
@@ -2803,8 +2838,8 @@
|
||||
"children": {
|
||||
"bondSkills.js": {
|
||||
"type": "file",
|
||||
"size": 41588,
|
||||
"sha256": "0b5a1ec26d8a8881057aa1fc8d95fecaf06fa6753d23c99a4978de83f8c44ec6"
|
||||
"size": 42902,
|
||||
"sha256": "cd0a38257095269f5cee24e246d7c5961b849026375fab6f9754468dd9df329c"
|
||||
},
|
||||
"config.js": {
|
||||
"type": "file",
|
||||
@@ -2813,8 +2848,8 @@
|
||||
},
|
||||
"extension.js": {
|
||||
"type": "file",
|
||||
"size": 76830,
|
||||
"sha256": "ae2585a6425cbcdf7cd2dee0f1151f5112fb8cfd4e8f6e089c6827774d6dbf9b"
|
||||
"size": 77106,
|
||||
"sha256": "02e3cdd9b05b7912ee0454b9aff918576e128c1294b64ea722dcd3d2c446a872"
|
||||
},
|
||||
"shuYing_Skill_Xvxiang1.mp3": {
|
||||
"type": "file",
|
||||
@@ -2825,18 +2860,18 @@
|
||||
},
|
||||
"update.js": {
|
||||
"type": "file",
|
||||
"size": 60005,
|
||||
"sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7"
|
||||
"size": 77689,
|
||||
"sha256": "07b1a8e332fdb996e47455b34106d09d749701bf3cd26dd843d016edc03ae266"
|
||||
},
|
||||
"update.txt": {
|
||||
"type": "file",
|
||||
"size": 3953,
|
||||
"sha256": "a81636c5b7c6dcf67b2e7b88b87668f3bb9626d974249c6e5413015cd1d13a21"
|
||||
"size": 4849,
|
||||
"sha256": "605be3429a648ecd676c85ac3e42598be129ab6a19d93b77fc34d7735e090f93"
|
||||
},
|
||||
"voices.js": {
|
||||
"type": "file",
|
||||
"size": 1711,
|
||||
"sha256": "975c58fe201cbbc0027af5038504064382a195df278085a75eca03b2ddace75a"
|
||||
"size": 1756,
|
||||
"sha256": "e25930cca7ea95af9337d01a098795c1e14dd2c9628baaeb0b7c7c140fca9c9f"
|
||||
},
|
||||
"yuanshen": {
|
||||
"type": "directory",
|
||||
@@ -2975,8 +3010,8 @@
|
||||
"title": "梦三模式",
|
||||
"entry": "mengsan/register.js",
|
||||
"manifest": "dist/modules/mengsan.json",
|
||||
"file_count": 457,
|
||||
"size_bytes": 705652979,
|
||||
"file_count": 763,
|
||||
"size_bytes": 147712610,
|
||||
"legacy_manifest": "dist/modules/mengsan.legacy.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.0.1.1-rc.1",
|
||||
"version": "2.0.2.3-rc.3",
|
||||
"module_id": "mengsan",
|
||||
"snapshot": "legacy",
|
||||
"files": [
|
||||
@@ -302,6 +302,305 @@
|
||||
"mengsan/assets/portraits/wriggler-forest-portrait.png",
|
||||
"mengsan/assets/portraits/wriggler-original.webp",
|
||||
"mengsan/assets/portraits/zhangfei-portrait.jpg",
|
||||
"mengsan/assets/relics/akabeko.png",
|
||||
"mengsan/assets/relics/alchemical_coffer.png",
|
||||
"mengsan/assets/relics/amethyst_aubergine.png",
|
||||
"mengsan/assets/relics/anchor.png",
|
||||
"mengsan/assets/relics/arcane_scroll.png",
|
||||
"mengsan/assets/relics/archaic_tooth.png",
|
||||
"mengsan/assets/relics/art_of_war.png",
|
||||
"mengsan/assets/relics/astrolabe.png",
|
||||
"mengsan/assets/relics/bag_of_marbles.png",
|
||||
"mengsan/assets/relics/bag_of_preparation.png",
|
||||
"mengsan/assets/relics/beating_remnant.png",
|
||||
"mengsan/assets/relics/beautiful_bracelet.png",
|
||||
"mengsan/assets/relics/bellows.png",
|
||||
"mengsan/assets/relics/belt_buckle.png",
|
||||
"mengsan/assets/relics/big_hat.png",
|
||||
"mengsan/assets/relics/big_mushroom.png",
|
||||
"mengsan/assets/relics/biiig_hug.png",
|
||||
"mengsan/assets/relics/bing_bong.png",
|
||||
"mengsan/assets/relics/black_blood.png",
|
||||
"mengsan/assets/relics/black_star.png",
|
||||
"mengsan/assets/relics/blessed_antler.png",
|
||||
"mengsan/assets/relics/blood_soaked_rose.png",
|
||||
"mengsan/assets/relics/blood_vial.png",
|
||||
"mengsan/assets/relics/bone_flute.png",
|
||||
"mengsan/assets/relics/bone_tea.png",
|
||||
"mengsan/assets/relics/book_of_five_rings.png",
|
||||
"mengsan/assets/relics/book_repair_knife.png",
|
||||
"mengsan/assets/relics/bookmark.png",
|
||||
"mengsan/assets/relics/booming_conch.png",
|
||||
"mengsan/assets/relics/bound_phylactery.png",
|
||||
"mengsan/assets/relics/bowler_hat.png",
|
||||
"mengsan/assets/relics/bread.png",
|
||||
"mengsan/assets/relics/brilliant_scarf.png",
|
||||
"mengsan/assets/relics/brimstone.png",
|
||||
"mengsan/assets/relics/bronze_scales.png",
|
||||
"mengsan/assets/relics/burning_blood.png",
|
||||
"mengsan/assets/relics/burning_sticks.png",
|
||||
"mengsan/assets/relics/byrdpip.png",
|
||||
"mengsan/assets/relics/calling_bell.png",
|
||||
"mengsan/assets/relics/candelabra.png",
|
||||
"mengsan/assets/relics/captains_wheel.png",
|
||||
"mengsan/assets/relics/cauldron.png",
|
||||
"mengsan/assets/relics/centennial_puzzle.png",
|
||||
"mengsan/assets/relics/chandelier.png",
|
||||
"mengsan/assets/relics/charons_ashes.png",
|
||||
"mengsan/assets/relics/chemical_x.png",
|
||||
"mengsan/assets/relics/choices_paradox.png",
|
||||
"mengsan/assets/relics/chosen_cheese.png",
|
||||
"mengsan/assets/relics/circlet.png",
|
||||
"mengsan/assets/relics/claws.png",
|
||||
"mengsan/assets/relics/cloak_clasp.png",
|
||||
"mengsan/assets/relics/cracked_core.png",
|
||||
"mengsan/assets/relics/crossbow.png",
|
||||
"mengsan/assets/relics/cursed_pearl.png",
|
||||
"mengsan/assets/relics/darkstone_periapt.png",
|
||||
"mengsan/assets/relics/data_disk.png",
|
||||
"mengsan/assets/relics/daughter_of_the_wind.png",
|
||||
"mengsan/assets/relics/delicate_frond.png",
|
||||
"mengsan/assets/relics/demon_tongue.png",
|
||||
"mengsan/assets/relics/diamond_diadem.png",
|
||||
"mengsan/assets/relics/dingy_rug.png",
|
||||
"mengsan/assets/relics/distinguished_cape.png",
|
||||
"mengsan/assets/relics/divine_destiny.png",
|
||||
"mengsan/assets/relics/divine_right.png",
|
||||
"mengsan/assets/relics/dollys_mirror.png",
|
||||
"mengsan/assets/relics/dowsing_rod.png",
|
||||
"mengsan/assets/relics/dragon_fruit.png",
|
||||
"mengsan/assets/relics/dream_catcher.png",
|
||||
"mengsan/assets/relics/driftwood.png",
|
||||
"mengsan/assets/relics/dusty_tome.png",
|
||||
"mengsan/assets/relics/ectoplasm.png",
|
||||
"mengsan/assets/relics/electric_shrymp.png",
|
||||
"mengsan/assets/relics/ember_tea.png",
|
||||
"mengsan/assets/relics/emotion_chip.png",
|
||||
"mengsan/assets/relics/empty_cage.png",
|
||||
"mengsan/assets/relics/eternal_feather.png",
|
||||
"mengsan/assets/relics/fake_anchor.png",
|
||||
"mengsan/assets/relics/fake_blood_vial.png",
|
||||
"mengsan/assets/relics/fake_happy_flower.png",
|
||||
"mengsan/assets/relics/fake_lees_waffle.png",
|
||||
"mengsan/assets/relics/fake_mango.png",
|
||||
"mengsan/assets/relics/fake_merchants_rug.png",
|
||||
"mengsan/assets/relics/fake_orichalcum.png",
|
||||
"mengsan/assets/relics/fake_snecko_eye.png",
|
||||
"mengsan/assets/relics/fake_strike_dummy.png",
|
||||
"mengsan/assets/relics/fake_venerable_tea_set.png",
|
||||
"mengsan/assets/relics/fencing_manual.png",
|
||||
"mengsan/assets/relics/festive_popper.png",
|
||||
"mengsan/assets/relics/fiddle.png",
|
||||
"mengsan/assets/relics/fishing_rod.png",
|
||||
"mengsan/assets/relics/forgotten_soul.png",
|
||||
"mengsan/assets/relics/fragrant_mushroom.png",
|
||||
"mengsan/assets/relics/fresnel_lens.png",
|
||||
"mengsan/assets/relics/frozen_egg.png",
|
||||
"mengsan/assets/relics/funerary_mask.png",
|
||||
"mengsan/assets/relics/fur_coat.png",
|
||||
"mengsan/assets/relics/galactic_dust.png",
|
||||
"mengsan/assets/relics/gambling_chip.png",
|
||||
"mengsan/assets/relics/game_piece.png",
|
||||
"mengsan/assets/relics/ghost_seed.png",
|
||||
"mengsan/assets/relics/girya.png",
|
||||
"mengsan/assets/relics/glass_eye.png",
|
||||
"mengsan/assets/relics/glitter.png",
|
||||
"mengsan/assets/relics/gnarled_hammer.png",
|
||||
"mengsan/assets/relics/gold_plated_cables.png",
|
||||
"mengsan/assets/relics/golden_compass.png",
|
||||
"mengsan/assets/relics/golden_pearl.png",
|
||||
"mengsan/assets/relics/gorget.png",
|
||||
"mengsan/assets/relics/gremlin_horn.png",
|
||||
"mengsan/assets/relics/hand_drill.png",
|
||||
"mengsan/assets/relics/happy_flower.png",
|
||||
"mengsan/assets/relics/hefty_tablet.png",
|
||||
"mengsan/assets/relics/helical_dart.png",
|
||||
"mengsan/assets/relics/history_course.png",
|
||||
"mengsan/assets/relics/horn_cleat.png",
|
||||
"mengsan/assets/relics/ice_cream.png",
|
||||
"mengsan/assets/relics/infused_core.png",
|
||||
"mengsan/assets/relics/intimidating_helmet.png",
|
||||
"mengsan/assets/relics/iron_club.png",
|
||||
"mengsan/assets/relics/ivory_tile.png",
|
||||
"mengsan/assets/relics/jeweled_mask.png",
|
||||
"mengsan/assets/relics/jewelry_box.png",
|
||||
"mengsan/assets/relics/joss_paper.png",
|
||||
"mengsan/assets/relics/juzu_bracelet.png",
|
||||
"mengsan/assets/relics/kaleidoscope.png",
|
||||
"mengsan/assets/relics/kifuda.png",
|
||||
"mengsan/assets/relics/kunai.png",
|
||||
"mengsan/assets/relics/kusarigama.png",
|
||||
"mengsan/assets/relics/lantern.png",
|
||||
"mengsan/assets/relics/large_capsule.png",
|
||||
"mengsan/assets/relics/lasting_candy.png",
|
||||
"mengsan/assets/relics/lava_lamp.png",
|
||||
"mengsan/assets/relics/lava_rock.png",
|
||||
"mengsan/assets/relics/lead_paperweight.png",
|
||||
"mengsan/assets/relics/leafy_poultice.png",
|
||||
"mengsan/assets/relics/lees_waffle.png",
|
||||
"mengsan/assets/relics/letter_opener.png",
|
||||
"mengsan/assets/relics/lizard_tail.png",
|
||||
"mengsan/assets/relics/looming_fruit.png",
|
||||
"mengsan/assets/relics/lords_parasol.png",
|
||||
"mengsan/assets/relics/lost_coffer.png",
|
||||
"mengsan/assets/relics/lost_wisp.png",
|
||||
"mengsan/assets/relics/lucky_fysh.png",
|
||||
"mengsan/assets/relics/lunar_pastry.png",
|
||||
"mengsan/assets/relics/mango.png",
|
||||
"mengsan/assets/relics/manifest.json",
|
||||
"mengsan/assets/relics/massive_scroll.png",
|
||||
"mengsan/assets/relics/maw_bank.png",
|
||||
"mengsan/assets/relics/meal_ticket.png",
|
||||
"mengsan/assets/relics/meat_cleaver.png",
|
||||
"mengsan/assets/relics/meat_on_the_bone.png",
|
||||
"mengsan/assets/relics/membership_card.png",
|
||||
"mengsan/assets/relics/mercury_hourglass.png",
|
||||
"mengsan/assets/relics/metronome.png",
|
||||
"mengsan/assets/relics/mini_regent.png",
|
||||
"mengsan/assets/relics/miniature_cannon.png",
|
||||
"mengsan/assets/relics/miniature_tent.png",
|
||||
"mengsan/assets/relics/molten_egg.png",
|
||||
"mengsan/assets/relics/mr_struggles.png",
|
||||
"mengsan/assets/relics/mummified_hand.png",
|
||||
"mengsan/assets/relics/music_box.png",
|
||||
"mengsan/assets/relics/mystic_lighter.png",
|
||||
"mengsan/assets/relics/neows_bones.png",
|
||||
"mengsan/assets/relics/neows_sacrifice.png",
|
||||
"mengsan/assets/relics/neows_talisman.png",
|
||||
"mengsan/assets/relics/neows_torment.png",
|
||||
"mengsan/assets/relics/new_leaf.png",
|
||||
"mengsan/assets/relics/ninja_scroll.png",
|
||||
"mengsan/assets/relics/nunchaku.png",
|
||||
"mengsan/assets/relics/nutritious_oyster.png",
|
||||
"mengsan/assets/relics/nutritious_soup.png",
|
||||
"mengsan/assets/relics/oddly_smooth_stone.png",
|
||||
"mengsan/assets/relics/old_coin.png",
|
||||
"mengsan/assets/relics/orange_dough.png",
|
||||
"mengsan/assets/relics/orichalcum.png",
|
||||
"mengsan/assets/relics/ornamental_fan.png",
|
||||
"mengsan/assets/relics/orrery.png",
|
||||
"mengsan/assets/relics/paels_blood.png",
|
||||
"mengsan/assets/relics/paels_claw.png",
|
||||
"mengsan/assets/relics/paels_eye.png",
|
||||
"mengsan/assets/relics/paels_flesh.png",
|
||||
"mengsan/assets/relics/paels_growth.png",
|
||||
"mengsan/assets/relics/paels_horn.png",
|
||||
"mengsan/assets/relics/paels_legion.png",
|
||||
"mengsan/assets/relics/paels_tears.png",
|
||||
"mengsan/assets/relics/paels_tooth.png",
|
||||
"mengsan/assets/relics/paels_wing.png",
|
||||
"mengsan/assets/relics/pandoras_box.png",
|
||||
"mengsan/assets/relics/pantograph.png",
|
||||
"mengsan/assets/relics/paper_krane.png",
|
||||
"mengsan/assets/relics/paper_phrog.png",
|
||||
"mengsan/assets/relics/parrying_shield.png",
|
||||
"mengsan/assets/relics/pear.png",
|
||||
"mengsan/assets/relics/pen_nib.png",
|
||||
"mengsan/assets/relics/pendulum.png",
|
||||
"mengsan/assets/relics/permafrost.png",
|
||||
"mengsan/assets/relics/petrified_toad.png",
|
||||
"mengsan/assets/relics/phial_holster.png",
|
||||
"mengsan/assets/relics/philosophers_stone.png",
|
||||
"mengsan/assets/relics/phylactery_unbound.png",
|
||||
"mengsan/assets/relics/planisphere.png",
|
||||
"mengsan/assets/relics/pocketwatch.png",
|
||||
"mengsan/assets/relics/pollinous_core.png",
|
||||
"mengsan/assets/relics/pomander.png",
|
||||
"mengsan/assets/relics/potion_belt.png",
|
||||
"mengsan/assets/relics/power_cell.png",
|
||||
"mengsan/assets/relics/prayer_wheel.png",
|
||||
"mengsan/assets/relics/precarious_shears.png",
|
||||
"mengsan/assets/relics/precise_scissors.png",
|
||||
"mengsan/assets/relics/preserved_fog.png",
|
||||
"mengsan/assets/relics/prismatic_gem.png",
|
||||
"mengsan/assets/relics/pumpkin_candle.png",
|
||||
"mengsan/assets/relics/punch_dagger.png",
|
||||
"mengsan/assets/relics/radiant_pearl.png",
|
||||
"mengsan/assets/relics/rainbow_ring.png",
|
||||
"mengsan/assets/relics/razor_tooth.png",
|
||||
"mengsan/assets/relics/red_mask.png",
|
||||
"mengsan/assets/relics/red_skull.png",
|
||||
"mengsan/assets/relics/regal_pillow.png",
|
||||
"mengsan/assets/relics/regalite.png",
|
||||
"mengsan/assets/relics/reptile_trinket.png",
|
||||
"mengsan/assets/relics/ring_of_the_drake.png",
|
||||
"mengsan/assets/relics/ring_of_the_snake.png",
|
||||
"mengsan/assets/relics/ringing_triangle.png",
|
||||
"mengsan/assets/relics/ripple_basin.png",
|
||||
"mengsan/assets/relics/royal_poison.png",
|
||||
"mengsan/assets/relics/royal_stamp.png",
|
||||
"mengsan/assets/relics/ruined_helmet.png",
|
||||
"mengsan/assets/relics/runic_capacitor.png",
|
||||
"mengsan/assets/relics/runic_pyramid.png",
|
||||
"mengsan/assets/relics/sai.png",
|
||||
"mengsan/assets/relics/sand_castle.png",
|
||||
"mengsan/assets/relics/screaming_flagon.png",
|
||||
"mengsan/assets/relics/scroll_boxes.png",
|
||||
"mengsan/assets/relics/sea_glass.png",
|
||||
"mengsan/assets/relics/seal_of_gold.png",
|
||||
"mengsan/assets/relics/self_forming_clay.png",
|
||||
"mengsan/assets/relics/sere_talon.png",
|
||||
"mengsan/assets/relics/shovel.png",
|
||||
"mengsan/assets/relics/shuriken.png",
|
||||
"mengsan/assets/relics/signet_ring.png",
|
||||
"mengsan/assets/relics/silken_tress.png",
|
||||
"mengsan/assets/relics/silver_crucible.png",
|
||||
"mengsan/assets/relics/sling_of_courage.png",
|
||||
"mengsan/assets/relics/small_capsule.png",
|
||||
"mengsan/assets/relics/snecko_eye.png",
|
||||
"mengsan/assets/relics/snecko_skull.png",
|
||||
"mengsan/assets/relics/sozu.png",
|
||||
"mengsan/assets/relics/sparkling_rouge.png",
|
||||
"mengsan/assets/relics/spiked_gauntlets.png",
|
||||
"mengsan/assets/relics/stone_calendar.png",
|
||||
"mengsan/assets/relics/stone_cracker.png",
|
||||
"mengsan/assets/relics/stone_humidifier.png",
|
||||
"mengsan/assets/relics/storybook.png",
|
||||
"mengsan/assets/relics/strawberry.png",
|
||||
"mengsan/assets/relics/strike_dummy.png",
|
||||
"mengsan/assets/relics/sturdy_clamp.png",
|
||||
"mengsan/assets/relics/sword_of_jade.png",
|
||||
"mengsan/assets/relics/sword_of_stone.png",
|
||||
"mengsan/assets/relics/symbiotic_virus.png",
|
||||
"mengsan/assets/relics/tanxs_whistle.png",
|
||||
"mengsan/assets/relics/tea_of_discourtesy.png",
|
||||
"mengsan/assets/relics/the_abacus.png",
|
||||
"mengsan/assets/relics/the_boot.png",
|
||||
"mengsan/assets/relics/the_courier.png",
|
||||
"mengsan/assets/relics/throwing_axe.png",
|
||||
"mengsan/assets/relics/tingsha.png",
|
||||
"mengsan/assets/relics/tiny_mailbox.png",
|
||||
"mengsan/assets/relics/toasty_mittens.png",
|
||||
"mengsan/assets/relics/toolbox.png",
|
||||
"mengsan/assets/relics/touch_of_orobas.png",
|
||||
"mengsan/assets/relics/tough_bandages.png",
|
||||
"mengsan/assets/relics/toxic_egg.png",
|
||||
"mengsan/assets/relics/toy_box.png",
|
||||
"mengsan/assets/relics/tri_boomerang.png",
|
||||
"mengsan/assets/relics/tungsten_rod.png",
|
||||
"mengsan/assets/relics/tuning_fork.png",
|
||||
"mengsan/assets/relics/twisted_funnel.png",
|
||||
"mengsan/assets/relics/unceasing_top.png",
|
||||
"mengsan/assets/relics/undying_sigil.png",
|
||||
"mengsan/assets/relics/unsettling_lamp.png",
|
||||
"mengsan/assets/relics/vajra.png",
|
||||
"mengsan/assets/relics/vambrace.png",
|
||||
"mengsan/assets/relics/velvet_choker.png",
|
||||
"mengsan/assets/relics/venerable_tea_set.png",
|
||||
"mengsan/assets/relics/very_hot_cocoa.png",
|
||||
"mengsan/assets/relics/vexing_puzzlebox.png",
|
||||
"mengsan/assets/relics/vitruvian_minion.png",
|
||||
"mengsan/assets/relics/war_hammer.png",
|
||||
"mengsan/assets/relics/war_paint.png",
|
||||
"mengsan/assets/relics/whetstone.png",
|
||||
"mengsan/assets/relics/whispering_earring.png",
|
||||
"mengsan/assets/relics/white_beast_statue.png",
|
||||
"mengsan/assets/relics/white_star.png",
|
||||
"mengsan/assets/relics/wing_charm.png",
|
||||
"mengsan/assets/relics/winged_boots.png",
|
||||
"mengsan/assets/relics/wongo_customer_appreciation_badge.png",
|
||||
"mengsan/assets/relics/wongos_mystery_ticket.png",
|
||||
"mengsan/assets/relics/yummy_cookie.png",
|
||||
"mengsan/assets/sushuang-zhangshiping.png",
|
||||
"mengsan/battle/battle-director.js",
|
||||
"mengsan/battle/battle-flow.js",
|
||||
@@ -309,6 +608,7 @@
|
||||
"mengsan/battle/battle-resources.js",
|
||||
"mengsan/battle/battle-session.js",
|
||||
"mengsan/battle/battle-settlement.js",
|
||||
"mengsan/battle/camps.js",
|
||||
"mengsan/battle/combat-rules.js",
|
||||
"mengsan/battle/death-blow.js",
|
||||
"mengsan/battle/engine-session.js",
|
||||
@@ -452,9 +752,14 @@
|
||||
"mengsan/progression/story.js",
|
||||
"mengsan/register.js",
|
||||
"mengsan/relics/battle.js",
|
||||
"mengsan/relics/catalog.js",
|
||||
"mengsan/relics/combat.js",
|
||||
"mengsan/relics/definitions.js",
|
||||
"mengsan/relics/hooks.js",
|
||||
"mengsan/relics/progression.js",
|
||||
"mengsan/relics/random.js",
|
||||
"mengsan/relics/rooms.js",
|
||||
"mengsan/relics/rules.js",
|
||||
"mengsan/runtime/battle-director.js",
|
||||
"mengsan/runtime/battle-flow.js",
|
||||
"mengsan/runtime/battle-loop.js",
|
||||
@@ -498,6 +803,7 @@
|
||||
"mengsan/ui/map/index.js",
|
||||
"mengsan/ui/map/renderer.js",
|
||||
"mengsan/ui/navigation.js",
|
||||
"mengsan/ui/relic-icon.js",
|
||||
"mengsan/ui/relics.js",
|
||||
"mengsan/ui/reward-package.js",
|
||||
"mengsan/ui/reward-ui.js",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# 目标
|
||||
|
||||
落实用户已经接受的术樱包技能审查结果及逐项确认的青青子衿规则修正;连环传导允许再次增伤,保留用户确认的增强版本。
|
||||
|
||||
# 范围
|
||||
|
||||
沿用用户选择的当前目录 D:/Work/noname-shuying,保留既有未提交修改。修复 tianshu/bondSkills.js 的依赖导入、江东基业分组、舌战他弃过滤,青青子衿的驭布重复群伤/花色、狼顾回合限制/他弃过滤,character 奋威动态初始化,以及天书 Boss 白仪/九天得牌过滤。
|
||||
|
||||
扩展修复青青子衿狼顾、妄尊、夺玺、引雷的难度规则,张角体力,以及核实报告“描述与实现还存在的范围差异”全部条目:疑神随机装备、反馈逐点发动与来源范围、狼顾来源范围、暴虐满血结算与选人范围、雷击选人范围、谏征使用者与目标范围。同步静态描述、动态描述和选择提示;远虑阴间明确描述为视为受伤。
|
||||
|
||||
## Source coverage
|
||||
|
||||
来源边界为用户本轮编号决定及 docs/reviews/qingqingzijin-official-description-review-2026-10-12.md 中范围差异章节和张角体力;官网仅作为已核实规则依据,不把整篇活动公告、虚拟偶像或奖励纳入实现。
|
||||
|
||||
| 来源条目与位置 | 读取状态 | 需要保留的内容 | Spec 位置 | 验收 ID | 覆盖状态 | 理由或替代关系 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 用户1,狼顾 | complete | 阴间黑色,其余黑桃,成功恢复次数,描述切换 | 狼顾难度 | A11 | covered | 当前决定 |
|
||||
| 用户2,远虑 | complete | 阴间保留视为受伤,描述明确 | 增强与文本 | A19 | covered | 覆盖旧公告真实伤害 |
|
||||
| 用户3–5,无双/神戟/战甲 | complete | 保留当前增强版本及数值 | 增强与文本 | A19 | covered | 覆盖旧公告基准 |
|
||||
| 用户6,妄尊 | complete | 阴间造成伤害即反击,其余仍超过1点 | 妄尊难度 | A12 | covered | 当前决定 |
|
||||
| 用户7,夺玺 | complete | 阴间仅自己摸2,困难双方各摸1 | 夺玺难度 | A13 | covered | 当前决定 |
|
||||
| 用户8,引雷 | complete | 阴间不限回合,困难回合外 | 引雷难度 | A14 | covered | 当前决定 |
|
||||
| 用户额外,张角体力 | complete | 普通/困难/阴间8/16/24,起手和技能不变 | 张角体力 | A15 | covered | 当前决定 |
|
||||
| 范围差异:疑神 | complete | 从可获得装备中随机取牌 | 随机疑神 | A16 | covered | 用户授权报告全部条目 |
|
||||
| 范围差异:反馈 | complete | 每点伤害独立机会,允许友方来源 | 反馈逐点 | A17 | covered | 用户授权报告全部条目 |
|
||||
| 范围差异:狼顾 | complete | 获得其他角色牌均可触发 | 角色范围 | A18 | covered | 用户授权报告全部条目 |
|
||||
| 范围差异:暴虐 | complete | 满血仍失去1体力,可选择任意角色 | 角色范围 | A18 | covered | 用户授权报告全部条目 |
|
||||
| 范围差异:雷击 | complete | 可选择任意其他角色 | 角色范围 | A18 | covered | 用户授权报告全部条目 |
|
||||
| 范围差异:谏征 | complete | 其他角色用杀即可,无额外阵营限制 | 角色范围 | A18 | covered | 用户授权报告全部条目 |
|
||||
|
||||
# 非目标
|
||||
|
||||
排除梦三和停用原神。不添加显式 sourceSkill 到所有正常 subSkill,不修改允许连环再次增伤的规则,不重构无关代码,不改用户已有天书直伤/回复功能,不提交、推送或发布。
|
||||
|
||||
不回退远虑阴间虚拟伤害、无双增伤、神戟增强数值及战甲阴间摸3;不按2021公告删除用户确认的增强。
|
||||
|
||||
# 验收示例
|
||||
|
||||
- 羁绊的 filter、mod、async content 在没有开发者全局对象时,能通过模块导入获取引擎依赖。
|
||||
- 江东基业二三级对应子技能通过正确 group 挂载并执行,低等级不重复叠加。
|
||||
- 驭布对一张多目标杀只判定一次,成功时对各存活目标只造成一次伤害。
|
||||
- 驭布困难难度黑桃成功、梅花失败;阴间难度黑色成功、红色失败,动态描述与难度条件一致。
|
||||
- 狼顾失败后仅限制当前回合;黑桃成功时恢复次数,即使没有可弃手牌也不会错误占用次数。
|
||||
- 奋威开局及中途获得时各初始化2蓄力,同次持有不因入场/开局事件重复添加,不重置共享蓄力。
|
||||
- 舌战与狼顾仅从允许被发动者弃置的牌中随机选择。
|
||||
- 白仪与九天仅从允许被发动者获得的牌中随机选择。
|
||||
- 连环伤害允许再次增伤,既有牌伤增益不添加 notLink 排除。
|
||||
- 新增定向回归验证并运行相关现有天书测试,说明真实游戏/联机实测边界,保留无关改动。
|
||||
- 狼顾阴间梅花成功、红色失败,普通/困难仅黑桃成功;动态描述同步。
|
||||
- 妄尊阴间累计1点伤害即反击,困难1点不反击,未受伤时弃牌数仍为困难1/阴间2。
|
||||
- 夺玺阴间替换摸牌后仅自己摸2,困难双方各摸1;动态描述、提示同步。
|
||||
- 引雷困难自己回合不触发、阴间自己回合触发;描述同步回合条件。
|
||||
- 张角三难度体力和上限8/16/24,起始手牌6/8/10和追加技能保留。
|
||||
- 疑神随机获取各目标合法装备,正常难度分支只选一名敌人,阴间所有合格敌人各一张;禁止选择受保护装备。
|
||||
- 反馈受到多点伤害有逐点发动机会,可对友方来源发动,源牌耗尽后停止;自伤只获取自身装备。
|
||||
- 狼顾接受其他友方来源;暴虐满血也失去1体力并允许任意角色;雷击允许其他友方,谏征允许其他角色使用的杀,不增加阵营限制。
|
||||
- 远虑阴间保留unreal并明确描述;无双/神戟/战甲增强不回退,全部改动描述与提示一致。
|
||||
|
||||
# 已确认决定
|
||||
|
||||
用户接受审查报告并明确授权修复其中8项已确认问题;用户明确确认允许再次增伤。用户先前选择当前目录,继续沿用。此前只读审查已完成,其发现作为本次已确认修复清单;现在允许修改对应源码和必要测试。
|
||||
|
||||
用户最新明确规则:困难 hard 为黑桃,阴间 nightmare 为黑色;保留一张多目标杀只判定一次,默认非阴间保持黑桃。
|
||||
|
||||
2026-10-12 用户逐项明确授权修复上述规则及范围差异,要求同步文本;指定保留远虑/无双/神戟/战甲增强,并指定张角8/16/24。该指令构成本轮完整范围确认,无需再次询问同一选择。
|
||||
@@ -0,0 +1,103 @@
|
||||
# 术樱包技能修复
|
||||
|
||||
## Requirement: 羁绊依赖和分组
|
||||
|
||||
天书羁绊模块通过显式导入绑定引擎对象;各等级只挂载自己的正确子技能。
|
||||
|
||||
### Scenario: 非开发者环境的羁绊执行
|
||||
Acceptance: A1
|
||||
无全局 lib/get/game 时,已导入的羁绊 filter、mod、async content 使用模块绑定正常执行。
|
||||
|
||||
### Scenario: 江东升级后的效果
|
||||
Acceptance: A2
|
||||
二级、三级拥有正确 draw/discard/gain 子技能;升档后不同时叠加旧等级。
|
||||
|
||||
## Requirement: 驭布和狼顾
|
||||
|
||||
驭布对每次用杀执行一次判定,困难难度要求黑桃、阴间难度要求黑色;狼顾按回合限制次数,成功时恢复次数,随机他弃遵守保护。
|
||||
|
||||
### Scenario: 驭布多目标
|
||||
Acceptance: A3
|
||||
杀指定两个目标,只在首次指定目标事件判定一次;符合当前难度的判定成功后各存活目标受到一次伤害。
|
||||
|
||||
### Scenario: 驭布花色
|
||||
Acceptance: A4
|
||||
困难 hard 时黑桃成功、梅花与红色失败;阴间 nightmare 时黑桃与梅花成功、红色失败。动态描述困难显示黑桃、阴间显示黑色,默认非阴间保持黑桃。
|
||||
|
||||
### Scenario: 狼顾次数
|
||||
Acceptance: A5
|
||||
判定失败保留使用标记到当前全局回合结束;符合当前难度的判定成功移除标记,与原持牌角色是否有可弃手牌无关。
|
||||
|
||||
## Requirement: 动态获得和候选牌合法性
|
||||
|
||||
奋威初始化兼容中途获得,禁止开局/入场重复加初始值,保留其他技能已有共享蓄力。随机牌操作使用对应他弃/他取合法候选。
|
||||
|
||||
### Scenario: 奋威动态获得
|
||||
Acceptance: A6
|
||||
开局和中途获得均初始化2蓄力;入场和开局事件不会重复添加。已有共享蓄力不被重置。
|
||||
|
||||
### Scenario: 随机他弃
|
||||
Acceptance: A7
|
||||
舌战和狼顾目标混有可弃和受保护牌时,随机结果只属于允许被发动者弃置的集合。
|
||||
|
||||
### Scenario: 随机获得
|
||||
Acceptance: A8
|
||||
白仪和九天目标混有可取得和受保护牌时,随机结果只属于允许被发动者获得的集合。
|
||||
|
||||
## Requirement: 保持规则与验证
|
||||
|
||||
用户允许连环传导再次增伤。只修改审查确认问题,不触及梦三、不重构无关功能。
|
||||
|
||||
### Scenario: 连环再次增伤
|
||||
Acceptance: A9
|
||||
五虎等既有牌伤增益仍接受连环伤害并增加伤害值。
|
||||
|
||||
### Scenario: 回归验证
|
||||
Acceptance: A10
|
||||
在修改前证明定向问题复现,修复后相关验证通过,同时现有天书直伤、回复和换关轮次测试通过;报告是否进行真实游戏或联机验证。
|
||||
|
||||
## Requirement: 青青子衿难度与体力
|
||||
|
||||
普通normal对应官网休闲,困难hard对应官网普通,阴间nightmare对应官网困难。按用户逐项决定修复规则,保留明确指定的增强版本。
|
||||
|
||||
### Scenario: 狼顾难度
|
||||
Acceptance: A11
|
||||
普通、困难仅黑桃成功;阴间黑桃和梅花成功、红色失败。动态描述反映条件;成功恢复次数及合法他弃继续成立。
|
||||
|
||||
### Scenario: 妄尊难度
|
||||
Acceptance: A12
|
||||
阴间敌人在当前回合对自己造成过正伤害即可反击1点;普通、困难仍要求累计伤害超过1点。未受伤时普通、困难弃1,阴间弃2。描述与条件一致。
|
||||
|
||||
### Scenario: 夺玺难度
|
||||
Acceptance: A13
|
||||
其他角色摸牌阶段可以失去1点体力替换摸牌;困难自己与其各摸1,阴间仅自己摸2。玩家离场不再摸牌,选择提示及动态描述同步。
|
||||
|
||||
### Scenario: 引雷难度
|
||||
Acceptance: A14
|
||||
困难仅自己回合外失牌触发,阴间自己回合内外均可。普通不获得追加技能;随机横置未横置角色。描述显示对应回合条件。
|
||||
|
||||
### Scenario: 张角体力
|
||||
Acceptance: A15
|
||||
张角普通、困难、阴间的初始体力与上限分别为8、16、24。起始手牌6/8/10、基础技能与困难起追加引雷保留;其他BOSS配置不受影响。
|
||||
|
||||
## Requirement: 取牌方式和角色范围
|
||||
|
||||
随机获得从发动者合法候选中抽取;描述未限制阵营时,不把AI偏好作为硬性目标限制。
|
||||
|
||||
### Scenario: 随机疑神
|
||||
Acceptance: A16
|
||||
疑神替换回复后,普通/困难获取一名随机合格敌人的随机合法装备,阴间获取每名合格敌人的一张随机合法装备;不提供装备自选,也不获取受保护装备。
|
||||
|
||||
### Scenario: 反馈逐点
|
||||
Acceptance: A17
|
||||
受到N点伤害后有N次独立可选反馈机会,每次获取来源一张合法牌;来源包括友方,来源离场或没有合法牌时不发动。自伤只能取自身装备,不能重复获取手牌。
|
||||
|
||||
### Scenario: 角色范围
|
||||
Acceptance: A18
|
||||
狼顾获得任何其他角色牌可触发;暴虐满血时X为0仍失去1体力,不选择伤害目标,受伤时目标可以是任意存活角色;雷击可选任意其他角色;谏征接受任何其他角色的杀,只检查非原目标、攻击范围和手牌等实际条件。AI可以偏好敌方/保护友方,但不得限制用户可选范围。
|
||||
|
||||
## Requirement: 保留增强并同步文本
|
||||
|
||||
### Scenario: 增强与文本
|
||||
Acceptance: A19
|
||||
远虑阴间仍调用unreal并描述为视为受到过伤害;无双未响应增伤次数、神戟普通/困难/阴间额外摸2/3/4和额外杀1/2/2、战甲阴间摸3均保留。狼顾、妄尊、夺玺、引雷及所有本轮改动的静态/动态文本和选择提示与最终规则一致。
|
||||
@@ -0,0 +1,79 @@
|
||||
# 青青子衿 BOSS 官网对照核实
|
||||
|
||||
核实日期:2026-10-12。依据用户指定的[三国杀官网公告](https://www.sanguosha.com/news/20210916_6804_0316),网页显示发布时间为 2021-09-23。这里只核实该公告,不推定它代表当前官方客户端规则。
|
||||
|
||||
更新:用户已逐项确认修复范围。现已修正狼顾、妄尊、夺玺、引雷、张角体力,以及下述范围差异全部条目,并同步描述和提示。远虑阴间虚拟伤害、无双/神戟/战甲增强按用户决定保留,远虑文本已明确“视为”。下文“当前源码”及“本轮没有修改”是修复前核实记录;最终验收结果见 `../comet/changes/shuying-skill-tianshu-review/verification.md`。
|
||||
|
||||
难度映射:官网休闲 → 术樱普通(normal);官网普通 → 术樱困难(hard);官网困难 → 术樱阴间(nightmare)。范围为六名 BOSS、22 个不同技能,排除梦三及网页的虚拟偶像技能。
|
||||
|
||||
本轮没有修改技能源码、配置、测试或 Comet 正式验收文件。上轮 Comet 的 60 次通过是已确认修复范围的验收,不能证明所有技能与本公告相符。
|
||||
|
||||
## 已确认的主要差异
|
||||
|
||||
| 技能 | 公告基准(按术樱难度表达) | 当前源码及描述 | 位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| 狼顾 | 阴间判定接受黑色 | 所有难度仅接受黑桃;只有黑桃静态描述,未注册动态描述 | `skill.js:263`、`translate.js:16` |
|
||||
| 远虑 | 困难、阴间均为真实伤害 | 阴间额外传入 `unreal`;动态描述多出“过”字 | `skill.js:296`、`dynamicTranslate.js:24` |
|
||||
| 无双 | 无增伤条款 | 主技能另外增加未响应时伤害 +1,普通/困难每回合 1 次、阴间 2 次;描述包含此增强 | `skill.js:309`、`dynamicTranslate.js:32` |
|
||||
| 神戟 | 各难度额外摸牌均为 2;困难额外杀为 1 | 困难额外摸 3、额外杀 2;阴间额外摸 4。动态描述同样采用这些数值 | `skill.js:448`、`skill.js:459`、`dynamicTranslate.js:37` |
|
||||
| 战甲 | 阴间仍摸 2 | 阴间摸 3,动态描述也写 3 | `skill.js:533`、`dynamicTranslate.js:54` |
|
||||
| 妄尊 | 阴间反击门槛为造成过伤害 | 所有难度均要求累计伤害 >1;阴间造成恰好 1 点时不反击。动态描述同样未切换门槛 | `skill.js:769`、`skill.js:800`、`dynamicTranslate.js:78` |
|
||||
| 夺玺 | 阴间只由自己摸 2 | 阴间让自己与摸牌角色各摸 2;提示、动态描述也如此 | `skill.js:826`、`skill.js:862`、`dynamicTranslate.js:84` |
|
||||
| 引雷 | 困难仅回合外;阴间不限回合 | 源码在所有难度排除自己的回合;静态描述又没有回合外条件,没有动态描述 | `skill.js:1158`、`translate.js:54` |
|
||||
|
||||
这些是与指定公告的确定差异。是否保留包内增强属于产品决定,尤其无双;此前“允许再次增伤”的决定仍保留,本报告不据此擅自删除增强。
|
||||
|
||||
## 描述与实现还存在的范围差异
|
||||
|
||||
- **疑神**:描述与公告一致。源码 `skill.js:118–126` 使用可操作的 `gainPlayerCard` 选装备,未在代码中随机选装备;判定目标的困难分支随机选择的是有可获得装备的敌人。若按公告要求随机装备,需要更换选牌方式。
|
||||
- **反馈**:描述保留逐点触发含义;源码 `skill.js:158–174` 每个 `damageEnd` 只获得一张牌,未按 `trigger.num` 逐点处理,且只接受敌方来源。引擎伤害流程对一次多点伤害只有一个 `damageEnd`。这些限制未体现在描述中。
|
||||
- **狼顾**:除主要差异外,`skill.js:245、254` 把牌的原持有者限制为敌人;描述没有此限制。
|
||||
- **暴虐**:描述数值上限 3/4/5 正确。`skill.js:613` 在满血时完全跳过技能,因此不会执行最后的失去体力;`skill.js:625` 也把可伤害角色限制为敌人,描述未写该限制。
|
||||
- **雷击**:描述一致,`skill.js:1002–1005` 选人范围只含敌人。
|
||||
- **谏征**:描述正确切换黑色杀的接管规则;`skill.js:1063、1070` 还要求使用者为敌人,且已有目标含非敌人,描述没有这两项条件。
|
||||
|
||||
上述阵营限制可能是包内 AI/模式适配,但这是推断,不能作为与公告一致的证明。
|
||||
|
||||
## 22 个技能的覆盖结果
|
||||
|
||||
“核心一致”仅表示本轮未发现公告主要条款或描述的明确差异,不等于真实游戏、联机或所有交互已验证。
|
||||
|
||||
| BOSS | 技能 | 核实结果 |
|
||||
| --- | --- | --- |
|
||||
| 曹操 | 奸雄 | 核心一致 |
|
||||
| 曹操 | 凌霸 | 核心一致,阴间伤害切换正确 |
|
||||
| 曹操 | 疑神 | 描述一致,随机选装备方式不符 |
|
||||
| 司马懿 | 反馈 | 描述一致,逐点触发与来源范围不符 |
|
||||
| 司马懿 | 鬼才 | 核心一致 |
|
||||
| 司马懿 | 狼顾 | 阴间判定与描述不符,另有限制来源阵营 |
|
||||
| 司马懿 | 远虑 | 阴间使用虚拟伤害,另有描述错字 |
|
||||
| 吕布 | 马术 | 引用引擎标准技能,核心一致 |
|
||||
| 吕布 | 无双 | 包内额外增伤,无公告依据 |
|
||||
| 吕布 | 神戟 | 困难及阴间数值不符 |
|
||||
| 吕布 | 战甲 | 阴间摸牌数不符 |
|
||||
| 董卓 | 酒池 | 核心一致 |
|
||||
| 董卓 | 肉林 | 核心一致 |
|
||||
| 董卓 | 暴虐 | 描述及上限正确,满血触发及选人范围不符 |
|
||||
| 董卓 | 驭布 | 困难黑桃、阴间黑色;描述与修复后实现一致 |
|
||||
| 袁术 | 庸肆 | 核心条款及描述一致 |
|
||||
| 袁术 | 妄尊 | 阴间反击条件不符 |
|
||||
| 袁术 | 夺玺 | 阴间摸牌对象不符 |
|
||||
| 张角 | 鬼道 | 核心一致 |
|
||||
| 张角 | 雷击 | 描述一致,选人范围不符 |
|
||||
| 张角 | 谏征 | 难度描述一致,额外阵营限制未写明 |
|
||||
| 张角 | 引雷 | 困难描述缺条件,阴间实现多条件 |
|
||||
|
||||
## 顺带核实的难度配置
|
||||
|
||||
`tianshu/config.js:116–121` 中六名 BOSS 追加技能均从困难开始授予,与映射后的公告一致。起始手牌 6/8/10 一致。五名 BOSS 的体力 10/20/30 一致;张角公告为 8/16/24,当前原始体力 4 加共用模板 6/16/26,实际配置为 10/20/30。
|
||||
|
||||
张角相关位置:`pve/boss/sets/qingqingzijin/character.js:60`、`tianshu/config.js:56`、`tianshu/config.js:121`;体力生成链:`tianshu/extension.js:438–439`。
|
||||
|
||||
## 验证证据与限制
|
||||
|
||||
- 成功下载官方原始 HTML,逐表核对六名 BOSS 的三个难度;网页桌面版/移动版重复表格内容相同。
|
||||
- 对现有真实模块进行临时定向调用:确认神戟三难度数值、阴间战甲摸 3、狼顾梅花失败、妄尊恰好 1 点不反击、夺玺两方各摸 2、远虑传入 `unreal`、引雷自己回合不触发、暴虐满血不触发。
|
||||
- 引擎 `noname/library/element/content.js:11120` 在 `unreal` 时不扣体力,所以远虑差异影响实际体力,并非日志措辞差异。
|
||||
- 驭布沿用当前候选的难度与多目标回归证据。本轮未重复执行整个修复套件,也未进行游戏或联机实测。
|
||||
|
||||
所有未列明完整路径的技能文件,均位于 `D:/Work/noname-shuying/pve/boss/sets/qingqingzijin/`。
|
||||
@@ -1096,6 +1096,109 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新设置:沿用选择窗口材质,两个选项与取消/确认按钮适配窄屏。 */
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] {
|
||||
--setting-muted: #d1c9bc;
|
||||
--setting-focus: #ffe2a0;
|
||||
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
|
||||
max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)) !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog {
|
||||
width: min(600px, 100%) !important;
|
||||
max-height: calc(100vh - 32px) !important;
|
||||
max-height: calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom))) !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-title,
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro,
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls {
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro,
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-tag,
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description {
|
||||
color: var(--setting-muted) !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
min-height: 0 !important;
|
||||
gap: 12px !important;
|
||||
padding: 18px !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card {
|
||||
min-height: 170px !important;
|
||||
padding: 38px 18px 18px !important;
|
||||
transform: none !important;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-order {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-name {
|
||||
margin-top: 0 !important;
|
||||
font-size: 25px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description {
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls {
|
||||
gap: 12px !important;
|
||||
padding: 0 18px 18px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 48px !important;
|
||||
height: auto !important;
|
||||
padding: 10px 8px !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-cancel {
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
border-color: rgba(232, 190, 99, 0.4) !important;
|
||||
color: var(--setting-muted) !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] button:focus-visible {
|
||||
outline: 2px solid var(--setting-focus) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] button:active:not(:disabled) {
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm:disabled {
|
||||
opacity: 0.5 !important;
|
||||
cursor: default !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 520px) {
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options {
|
||||
grid-template-columns: minmax(0, 1fr) !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card {
|
||||
min-height: 140px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 天书对局羁绊入口与概览:不依赖十周年 UI 节点,原版和扩展 UI 共用。 */
|
||||
.shuYing-bond-overview-button {
|
||||
all: unset !important;
|
||||
|
||||
@@ -2,10 +2,11 @@ import { lib, game, ui, get, ai, _status } from "../../noname.js";
|
||||
import updater from "./update.js"
|
||||
import func from "./function.js"
|
||||
import initShuYingMenu from "./menu.js";
|
||||
import { configureDiagnostics } from "./diagnostics/index.js";
|
||||
|
||||
export const type = "extension";
|
||||
export let shuYing = new Object();
|
||||
const shuYingLocalVersion = "2.0.2.3";
|
||||
const shuYingLocalVersion = "2.0.2.3-rc.3";
|
||||
|
||||
shuYing.name = "术樱包";
|
||||
shuYing.editable = false;
|
||||
@@ -16,6 +17,11 @@ shuYing.text = {}; //中上方文本
|
||||
shuYing.text_style = {}; //中上方文本样式
|
||||
shuYing.moduleCache = {}; //动态加载模块缓存,避免同一文件重复import
|
||||
shuYing.voices = {}; //语音台词表,voices.js缺失时保持为空
|
||||
shuYing.diagnostics = configureDiagnostics({
|
||||
lib, game, window, document, version: shuYingLocalVersion,
|
||||
extensionRoot: shuYing.url,
|
||||
});
|
||||
const diagnostics = shuYing.diagnostics.scope("extension");
|
||||
|
||||
//按需加载扩展内部模块。optional为true时,文件缺失只关闭对应功能,不中断整个扩展入口。
|
||||
shuYing.loadModule = async (path, label, optional = true) => {
|
||||
@@ -24,6 +30,7 @@ shuYing.loadModule = async (path, label, optional = true) => {
|
||||
.then(module => module.default ?? module)
|
||||
.catch(error => {
|
||||
const message = `术樱包:${label || path}加载失败`;
|
||||
diagnostics.error("module.load.failed", error, { path, label, optional });
|
||||
if (!optional) throw error;
|
||||
console.warn(message, error);
|
||||
return null;
|
||||
@@ -125,6 +132,7 @@ shuYing.getVersion = (callback) => {
|
||||
.catch(error => {
|
||||
if (requestId != shuYing.versionRequestId) return null;
|
||||
console.error("术樱包获取在线版本失败:", error);
|
||||
diagnostics.error("version.lookup.failed", error);
|
||||
shuYing.updateOnlineVersionMenu(null, "获取失败");
|
||||
return null;
|
||||
});
|
||||
@@ -245,7 +253,7 @@ shuYing.initCharacter = async () => {
|
||||
}
|
||||
|
||||
//初始化游戏时
|
||||
shuYing.precontent = async () => {
|
||||
shuYing.precontent = () => diagnostics.step("precontent", async () => {
|
||||
await shuYing.loadVoices();
|
||||
shuYing.initCSS();
|
||||
shuYing.initFunction();
|
||||
@@ -273,7 +281,7 @@ shuYing.precontent = async () => {
|
||||
initShuYingMenu({ lib, game, ui, shuYing, updater });
|
||||
shuYing.getVersion();
|
||||
await shuYing.initCharacter();
|
||||
};
|
||||
});
|
||||
|
||||
shuYing.config = () => {
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { getDirectDamageSkillCount } from "./tianshu/directDamage.js";
|
||||
import { getSkillRecoverCount } from "./tianshu/skillRecover.js";
|
||||
|
||||
export default function initFunction(lib, game, ui, get, ai, _status, shuYing = {}) {
|
||||
game.shuying_getDiscardNamesNum = function () {
|
||||
return Array.from(ui.discardPile.childNodes)
|
||||
@@ -225,7 +228,8 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
};
|
||||
|
||||
// 创建可复用的对局单选界面;单击选中卡片,确认后返回对应键值。
|
||||
shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null } = {}) => {
|
||||
shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null,
|
||||
currentKey = null, cancelable = false, variant = "default", confirmText = "确定" } = {}) => {
|
||||
return new Promise(resolve => {
|
||||
const items = options.map(option => typeof option == "string" ? { key: option, name: option } : option)
|
||||
.filter(option => option?.key);
|
||||
@@ -240,18 +244,26 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
if (parent) parent.appendChild(node);
|
||||
return node;
|
||||
};
|
||||
const previousFocus = document.activeElement;
|
||||
const mask = createNode("div", "shuYing-option-dialog-mask", null, document.body);
|
||||
mask.dataset.variant = variant;
|
||||
const dialog = createNode("div", "shuYing-option-dialog", null, mask);
|
||||
dialog.setAttribute("role", "dialog");
|
||||
dialog.setAttribute("aria-modal", "true");
|
||||
dialog.setAttribute("aria-label", title);
|
||||
createNode("div", "shuYing-option-dialog-title", title, dialog);
|
||||
if (intro) createNode("div", "shuYing-option-dialog-intro", intro, dialog);
|
||||
const optionArea = createNode("div", "shuYing-option-dialog-options", null, dialog);
|
||||
const controls = createNode("div", "shuYing-option-dialog-controls", null, dialog);
|
||||
let closed = false;
|
||||
let onKeydown = null;
|
||||
let selectedKey = defaultKey && items.some(item => item.key == defaultKey) ? defaultKey : items[0].key;
|
||||
const finish = key => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (onKeydown) document.removeEventListener("keydown", onKeydown, true);
|
||||
mask.remove();
|
||||
if (cancelable && previousFocus?.isConnected) previousFocus.focus?.({ preventScroll: true });
|
||||
resolve(key);
|
||||
};
|
||||
const refreshSelection = () => {
|
||||
@@ -260,6 +272,7 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
card.classList.toggle("selected", selected);
|
||||
card.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
confirm.disabled = currentKey != null && selectedKey == currentKey;
|
||||
};
|
||||
items.forEach((option, index) => {
|
||||
const card = createNode("button", "shuYing-option-dialog-card", null, optionArea);
|
||||
@@ -276,14 +289,44 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
refreshSelection();
|
||||
});
|
||||
});
|
||||
const confirm = createNode("button", "shuYing-option-dialog-confirm", "确定", controls);
|
||||
if (cancelable) {
|
||||
const cancel = createNode("button", "shuYing-option-dialog-confirm shuYing-option-dialog-cancel", "取消", controls);
|
||||
cancel.type = "button";
|
||||
cancel.addEventListener("click", event => { event.stopPropagation(); finish(null); });
|
||||
}
|
||||
const confirm = createNode("button", "shuYing-option-dialog-confirm", confirmText, controls);
|
||||
confirm.type = "button";
|
||||
confirm.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
finish(selectedKey);
|
||||
if (!confirm.disabled) finish(selectedKey);
|
||||
});
|
||||
mask.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
if (cancelable && event.target == mask) finish(null);
|
||||
});
|
||||
mask.addEventListener("click", event => event.stopPropagation());
|
||||
dialog.addEventListener("click", event => event.stopPropagation());
|
||||
if (cancelable) {
|
||||
dialog.addEventListener("keydown", event => event.stopPropagation());
|
||||
onKeydown = event => {
|
||||
if (event.key == "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
finish(null);
|
||||
}
|
||||
else if (event.key == "Tab") {
|
||||
const buttons = Array.from(dialog.querySelectorAll("button:not(:disabled)"));
|
||||
const first = buttons[0], last = buttons[buttons.length - 1];
|
||||
if (!dialog.contains(document.activeElement)
|
||||
|| event.shiftKey && document.activeElement == first
|
||||
|| !event.shiftKey && document.activeElement == last) {
|
||||
event.preventDefault();
|
||||
(event.shiftKey ? last : first)?.focus();
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", onKeydown, true);
|
||||
}
|
||||
refreshSelection();
|
||||
const initial = Array.from(optionArea.querySelectorAll(".shuYing-option-dialog-card")).find(node => node.dataset.key == selectedKey)
|
||||
|| optionArea.querySelector(".shuYing-option-dialog-card");
|
||||
@@ -346,6 +389,14 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => matchesCondition(item, currentSkills));
|
||||
if (!condition.type && condition.not) return !matchesCondition(condition.not, currentSkills);
|
||||
if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) >= Math.max(1, Number(condition.count) || 1);
|
||||
if (condition.type == "directDamage") {
|
||||
return getDirectDamageSkillCount(player, currentSkills, lib)
|
||||
>= Math.max(1, Number(condition.count) || 1);
|
||||
}
|
||||
if (condition.type == "skillRecover") {
|
||||
return getSkillRecoverCount(player, currentSkills, lib)
|
||||
>= Math.max(1, Number(condition.count) || 1);
|
||||
}
|
||||
if (condition.type == "skill") return currentSkills.includes(condition.skill);
|
||||
if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name);
|
||||
if (condition.type == "skills") {
|
||||
@@ -387,6 +438,13 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => isConditionRelevant(item, currentSkills));
|
||||
if (!condition.type && condition.not) return false;
|
||||
if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) > 0;
|
||||
if (condition.type == "directDamage") {
|
||||
return getDirectDamageSkillCount(player, currentSkills, lib)
|
||||
> 0;
|
||||
}
|
||||
if (condition.type == "skillRecover") {
|
||||
return getSkillRecoverCount(player, currentSkills, lib) > 0;
|
||||
}
|
||||
if (condition.type == "skill") return currentSkills.includes(condition.skill);
|
||||
if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name);
|
||||
if (condition.type == "skills") return [...(condition.all || []), ...(condition.any || [])].some(skill => currentSkills.includes(skill));
|
||||
@@ -411,6 +469,19 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
const need = Math.max(1, Number(condition.count) || 1);
|
||||
return `包含【${condition.keyword}】的技能 ${Math.min(count, need)}/${need}`;
|
||||
}
|
||||
if (condition.type == "directDamage") {
|
||||
const count = getDirectDamageSkillCount(
|
||||
player, currentSkills, lib);
|
||||
const need = Math.max(1, Number(condition.count) || 1);
|
||||
return `已实际造成直伤的奖励技能 ${Math.min(count, need)}`
|
||||
+ `/${need};首次有效直伤后登记`;
|
||||
}
|
||||
if (condition.type == "skillRecover") {
|
||||
const count = getSkillRecoverCount(player, currentSkills, lib);
|
||||
const need = Math.max(1, Number(condition.count) || 1);
|
||||
return `已实际回复体力的奖励技能 ${Math.min(count, need)}`
|
||||
+ `/${need};首次有效技能回复后登记`;
|
||||
}
|
||||
if (condition.type == "skill") return `${currentSkills.includes(condition.skill) ? "已" : "未"}选择【${get.translation(condition.skill)}】`;
|
||||
if (condition.type == "skillName") return `${currentSkills.some(skill => lib.translate[skill] == condition.name) ? "已" : "未"}选择任意版本的【${condition.name}】`;
|
||||
if (condition.type == "skills") {
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
`register.js` 是扩展加载入口,`mode.js` 负责组装模式、注册技能并连接各领域模块,`config.js` 提供模式配置。发布器和外部扩展应继续从 `register.js` 加载,避免依赖内部文件路径。
|
||||
|
||||
梦三独立组装模式,不加载或继承身份模式。敌我关系统一由
|
||||
`battle/camps.js` 读取角色的 `storage.mengsanCamp_shuying`:主角、支援和
|
||||
羁绊助战为 `ally`,怪物及敌方援军为 `enemy`。`side` 与梦三专属的
|
||||
`identity` 标记只用于核心及界面兼容,不赋予主公、忠臣或反贼规则。
|
||||
|
||||
技能应使用 `isFriendOf` / `isEnemyOf`、`getFriends` / `getEnemies` 或
|
||||
`get.attitude` 判断敌我。基础态度为同阵营及自身 `+8`、对立阵营 `-8`,
|
||||
未设置有效阵营的其他对象为 `0`;剧情切换阵营后按新阵营判断。
|
||||
`get.effect`、`get.effect_use`、`get.damageEffect`、`get.recoverEffect` 和
|
||||
牌价值、技能效果修正继续使用核心算法,并通过上述态度计算行动收益。
|
||||
有益效果不一律视为正收益,伤害也不一律视为负收益,仍遵循技能自己的 AI 配置。
|
||||
|
||||
梦三不启用主公技,初始技能及动态授予技能均按此限制处理。阵营始终公开,
|
||||
出牌或技能的 `logAi` / `addExpose` 调用不推断隐藏身份;击杀不附加身份奖惩,
|
||||
胜负与奖励沿用梦三关卡和结算流程。
|
||||
|
||||
| 目录 | 职责 |
|
||||
| --- | --- |
|
||||
| `battle/` | 战斗会话、回合、结算、费用与通用战斗效果 |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- 批量优化只处理 `mengsan_*.png` 运行时卡牌,参考原件如 `wound-original.png` 不参与。
|
||||
- 画风:泛黄宣纸、黑褐水墨、飞白与淡赭色、中国古代人物或兵器;根据卡牌效果独立安排竖幅构图。拆包图只作题材参考,不修改拆包原件。
|
||||
- 立绘不绘制名称、费用、花色、强化角标或边框。左上花色点数、左侧中段费用、右下名称由游戏原生节点和梦三样式叠加;左侧信息栏和右下名称需留空间。
|
||||
- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。`mengsan_ic_`、`mengsan_colorless_`、`mengsan_event_`、`mengsan_status_`、`mengsan_curse_` 已按前缀接入 `ui/hand/style.css` 的本地完整卡面选择器;其他命名的新增卡牌须单独接入,长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。
|
||||
- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。手牌叠层按实际注册定义的 `fullimage: true` 及 `ext:术樱包/mengsan/assets/cards/牌名.png` 自动设置 `data-mengsan-local-art="full"`;四字及以上的翻译名称自动标记为长名,常规和窄屏样式统一,不再维护逐牌 CSS 名单。保持 `cover`,不使用 `100% 100%` 拉伸。
|
||||
- 费用和强化角标继续使用现有 `data-*` 与伪元素,避免向卡牌子节点写入额外文字而触发十周年 UI 扫描。
|
||||
- 不改变卡牌费用、效果、强化、奖励与存档数据;不修改全局标准/军争牌或十周年 UI。
|
||||
- 接入后检查完整立绘、花色/费用/名称、扇形叠牌、悬停放大和窄屏。文件检查、隔离行为测试及预览不代表实机验收。
|
||||
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.3 KiB |