mingding-snap-modifier 2.13.1 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +75 -27
- package/dist/mingding-modifier.js +7592 -2789
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14.js +7666 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_tavernhelper/345/257/274/345/205/245.json +13 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_/350/204/232/346/234/254/346/272/220/347/240/201/344/270/226/347/225/214/344/271/246.json +11 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_/351/223/276/346/216/245/347/211/210/344/270/226/347/225/214/344/271/246.json +29 -0
- package/docs/v2.14/350/277/255/344/273/243/350/257/264/346/230/216.md +143 -0
- package/package.json +6 -8
- package/dist/mingding-import.json +0 -13
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_13.js +0 -2863
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_13_tavernhelper/345/257/274/345/205/245.json +0 -13
- package/docs/v2.13/350/277/255/344/273/243/345/267/256/345/210/206/350/257/264/346/230/216.md +0 -40
|
@@ -1,2863 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
'use strict';
|
|
3
|
-
if (window.parent && window.parent.__MINGDING_MODIFIER__) { return; }
|
|
4
|
-
const P = window.parent || window;
|
|
5
|
-
P.__MINGDING_MODIFIER__ = true;
|
|
6
|
-
|
|
7
|
-
/* ============ 基础工具 ============ */
|
|
8
|
-
const $ = (sel, el) => (el || P.document).querySelector(sel);
|
|
9
|
-
const $$ = (sel, el) => Array.from((el || P.document).querySelectorAll(sel));
|
|
10
|
-
const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
11
|
-
// 防原型污染:拒绝危险键名(__proto__/constructor/prototype)
|
|
12
|
-
const isSafeKey = n => !!n && !/^(__proto__|constructor|prototype)$/i.test(String(n).trim());
|
|
13
|
-
const deepClone = o => { try { return JSON.parse(JSON.stringify(o)); } catch (e) { return {}; } };
|
|
14
|
-
const num = v => { const n = parseFloat(v); return isNaN(n) ? 0 : n; };
|
|
15
|
-
|
|
16
|
-
/* ============ v4.3.1 资源值适配(HP/MP/SP:新{当前,上限:{_基础,额外}} vs 旧单值) ============ */
|
|
17
|
-
// 是否新版嵌套对象结构
|
|
18
|
-
function isResObj(v) { return !!(v && typeof v === 'object' && !Array.isArray(v) && ('当前' in v || '上限' in v)); }
|
|
19
|
-
const resCur = v => isResObj(v) ? num(v['当前'] ?? 0) : num(v ?? 0); // 当前值
|
|
20
|
-
const resBase = v => isResObj(v) ? num(((v['上限'] || {})['_基础']) ?? 0) : null; // 基础上限(只读),旧版无
|
|
21
|
-
const resExtra = v => isResObj(v) ? num(((v['上限'] || {})['额外']) ?? 0) : null; // 额外上限,旧版无
|
|
22
|
-
const resTotal = (v, legacyMax) => isResObj(v) ? resBase(v) + resExtra(v) : num(legacyMax ?? 0); // 总上限
|
|
23
|
-
// 写入当前值:新版写 .当前(保留 .上限),旧版写单值 + 可选同步旧键
|
|
24
|
-
function setResCur(owner, key, val, legacyMax) {
|
|
25
|
-
if (isResObj(owner[key])) {
|
|
26
|
-
const u = owner[key]['上限'] || {};
|
|
27
|
-
owner[key] = { '当前': num(val), '上限': { '_基础': num(u['_基础'] ?? 0), '额外': num(u['额外'] ?? 0) } };
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
owner[key] = num(val);
|
|
31
|
-
if (legacyMax != null && owner[key + '上限'] !== undefined) owner[key + '上限'] = num(legacyMax);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
// 写入额外上限:新版写 .上限.额外,旧版写回旧键
|
|
35
|
-
function setResExtra(owner, key, val) {
|
|
36
|
-
if (isResObj(owner[key])) { const u = owner[key]['上限'] || {}; owner[key]['上限'] = { '_基础': num(u['_基础'] ?? 0), '额外': num(val) }; return true; }
|
|
37
|
-
if (owner[key + '上限'] !== undefined) owner[key + '上限'] = num(val);
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
// v4.3.1 资源对象模板(新增 NPC 用):{当前:0, 上限:{_基础:0,额外:0}}
|
|
41
|
-
function newResObj() { return { '当前': 0, '上限': { '_基础': 0, '额外': 0 } }; }
|
|
42
|
-
// 行下方内联展开:点击条目 → 该行正下方插入编辑区;再次点击收起;点击其它行切换
|
|
43
|
-
function inlineEditor(itRow, contentHtml) {
|
|
44
|
-
if (!itRow || !itRow.parentElement) return null;
|
|
45
|
-
const cur = itRow.nextElementSibling;
|
|
46
|
-
if (cur && cur.className && String(cur.className).includes('inline-editor')) { cur.remove(); return null; }
|
|
47
|
-
const open = itRow.parentElement.querySelector('.inline-editor');
|
|
48
|
-
if (open) open.remove();
|
|
49
|
-
const ed = P.document.createElement('div');
|
|
50
|
-
ed.className = 'inline-editor';
|
|
51
|
-
ed.style.cssText = 'border-top:1px dashed var(--md-line,#333a4d);margin:4px 0 6px;padding:8px 10px;background:var(--md-bg1,#14161f);border-radius:6px';
|
|
52
|
-
ed.innerHTML = contentHtml;
|
|
53
|
-
itRow.parentElement.insertBefore(ed, itRow.nextSibling);
|
|
54
|
-
return ed;
|
|
55
|
-
}
|
|
56
|
-
const toast = (msg, type = 'info') => {
|
|
57
|
-
try { const fn = P.toastr && P.toastr[type] ? P.toastr[type] : (P.toastr && P.toastr.info); fn && fn.call(P.toastr, msg, NAME); }
|
|
58
|
-
catch (e) { console.log('[' + NAME + ']', msg); }
|
|
59
|
-
};
|
|
60
|
-
const NAME = '命定之诗·全量修改器'; const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
61
|
-
|
|
62
|
-
/* ============ SVG 图标(全部内联绘制,无外部素材) ============ */
|
|
63
|
-
const ICONS = {
|
|
64
|
-
gear: '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 8.5A3.5 3.5 0 1 0 12 15.5 3.5 3.5 0 0 0 12 8.5Zm9.4 5.06a8.6 8.6 0 0 0 0-3.12l2.1-1.64a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.6-.22l-2.5 1a8.6 8.6 0 0 0-2.7-1.56L15.5 1.6a.5.5 0 0 0-.5-.4h-4a.5.5 0 0 0-.5.4l-.32 2.7a8.6 8.6 0 0 0-2.7 1.56l-2.5-1a.5.5 0 0 0-.6.22l-2 3.46a.5.5 0 0 0 .12.64l2.1 1.64a8.6 8.6 0 0 0 0 3.12l-2.1 1.64a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .6.22l2.5-1a8.6 8.6 0 0 0 2.7 1.56l.32 2.7a.5.5 0 0 0 .5.4h4a.5.5 0 0 0 .5-.4l.32-2.7a8.6 8.6 0 0 0 2.7-1.56l2.5 1a.5.5 0 0 0 .6-.22l2-3.46a.5.5 0 0 0-.12-.64Z"/></svg>',
|
|
65
|
-
user: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-3.33 0-10 1.67-10 5v3h20v-3c0-3.33-6.67-5-10-5Z"/></svg>',
|
|
66
|
-
group: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M9 11a4 4 0 1 0-4-4 4 4 0 0 0 4 4Zm8 1a3 3 0 1 0-3-3 3 3 0 0 0 3 3Zm-8 1c-2.67 0-8 1.34-8 4v2h10v-2c0-.7.09-1.25.27-1.7A7.8 7.8 0 0 0 9 13Zm8 1c-1.13 0-2.53.26-3.69.76A5.4 5.4 0 0 1 14 17v2h10v-2c0-2.66-5.34-4-7-4Z"/></svg>',
|
|
67
|
-
globe: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm7.9 9h-3.2a15 15 0 0 0-1.2-4.9A8.1 8.1 0 0 1 19.9 11ZM12 4c.9 1.1 1.7 3.5 1.9 7h-3.8c.2-3.5 1-5.9 1.9-7ZM4.1 13a8.1 8.1 0 0 1 4.4-4.9A15 15 0 0 0 7.3 13Zm0 2h3.2a15 15 0 0 0 1.2 4.9A8.1 8.1 0 0 1 4.1 15ZM12 20c-.9-1.1-1.7-3.5-1.9-7h3.8c-.2 3.5-1 5.9-1.9 7Zm4.3-5.1a15 15 0 0 0 1.2-4.9h3.2a8.1 8.1 0 0 1-4.4 4.9Z"/></svg>',
|
|
68
|
-
calendar: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 16H5V10h14Z"/></svg>',
|
|
69
|
-
palette: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 2.5 2.5 0 0 0 2.5-2.5c0-.6-.2-1.2-.6-1.6a2.3 2.3 0 0 1 1.7-3.9h3.1A3.9 3.9 0 0 0 22 10.1 8.1 8.1 0 0 0 12 2ZM6.5 13a1.5 1.5 0 1 1 1.5-1.5A1.5 1.5 0 0 1 6.5 13Zm3-5A1.5 1.5 0 1 1 11 6.5 1.5 1.5 0 0 1 9.5 8Zm5 0A1.5 1.5 0 1 1 16 6.5 1.5 1.5 0 0 1 14.5 8Zm3 4a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5Z"/></svg>',
|
|
70
|
-
terminal: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 15.6l-1.4-1.4L8.8 11 5.6 7.8 7 6.4l4.6 4.6Zm9 1.4h-6v-2h6Z"/></svg>',
|
|
71
|
-
plus: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6Z"/></svg>',
|
|
72
|
-
trash: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6ZM8 9h8v10H8Zm7.5-5-1-1h-5l-1 1H5v2h14V4Z"/></svg>',
|
|
73
|
-
edit: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M3 17.25V21h3.75L17.8 9.94l-3.75-3.75L3 17.25ZM20.7 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75Z"/></svg>',
|
|
74
|
-
heart: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09A6 6 0 0 1 16.5 3C19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54Z"/></svg>',
|
|
75
|
-
coin: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8Zm1-13h-2v1.5a3.5 3.5 0 0 0-1 6.8V17h4v-1.7a3.5 3.5 0 0 0 2-3.3h-2a1.5 1.5 0 0 1-3 0 1.5 1.5 0 0 1 1-1.4V12a1.5 1.5 0 0 1 2 1.4h2a3.5 3.5 0 0 0-3-3.4V7Z"/></svg>',
|
|
76
|
-
bag: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M20 8h-3V6a5 5 0 0 0-10 0v2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2ZM9 6a3 3 0 0 1 6 0v2H9Zm11 14H4V10h16Z"/></svg>',
|
|
77
|
-
sword: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M20 2h-7.2a1 1 0 0 0-.7.3L10 4.5a1 1 0 0 0 0 1.4l.9.9-7.5 7.5a1 1 0 0 0 0 1.4l3.9 3.9a1 1 0 0 0 1.4 0l7.5-7.5.9.9a1 1 0 0 0 1.4 0l2.2-2.1a1 1 0 0 0 .3-.7V4a2 2 0 0 0-2-2Zm-4.9 12.7-1.8-1.8 1.6-1.6 1.8 1.8Zm-3-3L6.7 17.1l-1.8-1.8 5.4-5.4 1.8 1.8Z"/></svg>',
|
|
78
|
-
spark: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M12 2 15 9l7 3-7 3-3 7-3-7-7-3 7-3Z"/></svg>',
|
|
79
|
-
refresh: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M17.65 6.35A7.95 7.95 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6a5.9 5.9 0 0 1 4.22 1.78L13 11h7V4Z"/></svg>',
|
|
80
|
-
question: '<svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm1 17h-2v-2h2Zm2.07-7.75-.9.92A3.4 3.4 0 0 0 13 14h-2v-.5a3.2 3.2 0 0 1 1-2.4l1.1-1.1a1.5 1.5 0 0 0-1-2.6A1.5 1.5 0 0 0 10.6 9H8.5a3.5 3.5 0 0 1 7 0 3.4 3.4 0 0 1-1.43 3.25Z"/></svg>',
|
|
81
|
-
check: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4Z"/></svg>',
|
|
82
|
-
cross: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12Z"/></svg>',
|
|
83
|
-
wand: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M7.5 5.6 5 7l1.4-2.5L5 2l2.5 1.4L10 2 8.6 4.5 10 7Zm12 11.2L5.8 20.8a1 1 0 0 1-1.2-1.2L8 5.9a1 1 0 0 1 1.7-.3l8.8 8.8a1 1 0 0 1 1 2.4ZM13 6l3-1.5L17.5 1 19 2.5 22.5 4 19 5.5 17.5 7Z"/></svg>',
|
|
84
|
-
shield: '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5Zm-2 16-4-4 1.41-1.41L10 14.17l6.59-6.58L18 9Z"/></svg>'
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/* ============ 变量访问层(ST 1.13 + MVU 兼容) ============ */
|
|
88
|
-
// tavern_helper 脚本运行在 iframe 中:iframe 内注入的 window.SillyTavern 是完整宿主对象
|
|
89
|
-
// (含 chat / getContext / saveChat / eventSource 等),主窗口的 SillyTavern 仅 { libs, getContext }
|
|
90
|
-
function hostObject() {
|
|
91
|
-
if (typeof window !== 'undefined' && window.SillyTavern) return window.SillyTavern;
|
|
92
|
-
if (P.SillyTavern) return P.SillyTavern;
|
|
93
|
-
if (P.parent && P.parent.SillyTavern) return P.parent.SillyTavern;
|
|
94
|
-
if (P.top && P.top.SillyTavern) return P.top.SillyTavern;
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
function getChatArray() {
|
|
98
|
-
try {
|
|
99
|
-
const h = hostObject();
|
|
100
|
-
if (h && Array.isArray(h.chat)) return h.chat;
|
|
101
|
-
if (h && h.getContext) { const ctx = h.getContext(); if (ctx && Array.isArray(ctx.chat)) return ctx.chat; }
|
|
102
|
-
} catch (e) { /* 忽略 */ }
|
|
103
|
-
return P.chat || [];
|
|
104
|
-
}
|
|
105
|
-
function getChatMetadata() {
|
|
106
|
-
try {
|
|
107
|
-
const h = hostObject();
|
|
108
|
-
if (h && h.chat_metadata && typeof h.chat_metadata === 'object') return h.chat_metadata;
|
|
109
|
-
if (h && h.getContext) { const ctx = h.getContext(); if (ctx && ctx.chatMetadata && typeof ctx.chatMetadata === 'object') return ctx.chatMetadata; }
|
|
110
|
-
} catch (e) { /* 忽略 */ }
|
|
111
|
-
return P.chat_metadata || {};
|
|
112
|
-
}
|
|
113
|
-
// 获取事件系统(用于监听 CHAT_CHANGED 等)
|
|
114
|
-
function getEventSource() {
|
|
115
|
-
try {
|
|
116
|
-
const h = hostObject();
|
|
117
|
-
if (h && h.eventSource) return h.eventSource;
|
|
118
|
-
if (h && h.getContext) { const ctx = h.getContext(); if (ctx && ctx.eventSource) return ctx.eventSource; }
|
|
119
|
-
} catch (e) { /* 忽略 */ }
|
|
120
|
-
return P.eventSource || null;
|
|
121
|
-
}
|
|
122
|
-
function getEventTypes() {
|
|
123
|
-
try {
|
|
124
|
-
const h = hostObject();
|
|
125
|
-
if (h && h.event_types) return h.event_types;
|
|
126
|
-
if (h && h.getContext) { const ctx = h.getContext(); if (ctx && ctx.eventTypes) return ctx.eventTypes; }
|
|
127
|
-
} catch (e) { /* 忽略 */ }
|
|
128
|
-
return P.event_types || null;
|
|
129
|
-
}
|
|
130
|
-
// 获取 MVU 引擎(MVU beta 挂在主窗口 window.Mvu / window.parent.Mvu)
|
|
131
|
-
function getMvu() {
|
|
132
|
-
return P.Mvu || (P.parent && P.parent.Mvu) || (P.top && P.top.Mvu) || null;
|
|
133
|
-
}
|
|
134
|
-
// 触发聊天保存(持久化到磁盘)
|
|
135
|
-
function saveChatNow() {
|
|
136
|
-
try {
|
|
137
|
-
const h = hostObject();
|
|
138
|
-
if (h && typeof h.saveChat === 'function') { h.saveChat(); return; }
|
|
139
|
-
if (h && h.getContext) { const ctx = h.getContext(); if (ctx && typeof ctx.saveChat === 'function') ctx.saveChat(); }
|
|
140
|
-
} catch (e) { console.log('[修改器] 保存触发失败:', e.message); }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 读取聊天中最近一条含指定变量的消息(兼容 variables 为 数组[swipe] 或 对象 两种形态)
|
|
144
|
-
function readLatestVar(name) {
|
|
145
|
-
const chat = getChatArray();
|
|
146
|
-
if (!Array.isArray(chat)) return undefined;
|
|
147
|
-
for (let i = chat.length - 1; i >= 0; i--) {
|
|
148
|
-
const msg = chat[i];
|
|
149
|
-
if (!msg) continue;
|
|
150
|
-
const v = msg.variables;
|
|
151
|
-
if (Array.isArray(v)) {
|
|
152
|
-
// MVU 形态:variables[swipe_id ?? 0] 内是 { stat_data: {...}, date: {...} }
|
|
153
|
-
for (const item of v) {
|
|
154
|
-
if (item && typeof item === 'object' && name in item) return deepClone(item[name]);
|
|
155
|
-
}
|
|
156
|
-
} else if (v && typeof v === 'object' && name in v) {
|
|
157
|
-
return deepClone(v[name]);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
// 写入最后一条消息的 variables(MVU 形态:variables[swipe_id ?? 0]),保留已有键
|
|
163
|
-
function writeLatestVar(name, value) {
|
|
164
|
-
const chat = getChatArray();
|
|
165
|
-
if (!Array.isArray(chat) || !chat.length) return false;
|
|
166
|
-
// 优先写最后一条 AI 消息(状态栏/正文显示读取的就是它),无 AI 消息时退回最后一条
|
|
167
|
-
let last = null;
|
|
168
|
-
for (let i = chat.length - 1; i >= 0; i--) {
|
|
169
|
-
const m = chat[i];
|
|
170
|
-
if (m && !m.is_user && typeof m.mes === 'string') { last = m; break; }
|
|
171
|
-
}
|
|
172
|
-
if (!last) last = chat[chat.length - 1];
|
|
173
|
-
if (!last || typeof last !== 'object') return false;
|
|
174
|
-
if (!Array.isArray(last.variables)) last.variables = [];
|
|
175
|
-
const idx = (typeof last.swipe_id === 'number' ? last.swipe_id : 0) || 0;
|
|
176
|
-
const bucket = last.variables[idx] && typeof last.variables[idx] === 'object' ? last.variables[idx] : {};
|
|
177
|
-
bucket[name] = value;
|
|
178
|
-
last.variables[idx] = bucket;
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
// 聊天级模板变量(chat_metadata.variables)
|
|
182
|
-
function readChatVars() {
|
|
183
|
-
const meta = getChatMetadata();
|
|
184
|
-
return (meta && meta.variables) ? meta.variables : {};
|
|
185
|
-
}
|
|
186
|
-
function writeChatVar(key, value) {
|
|
187
|
-
const meta = getChatMetadata();
|
|
188
|
-
if (!meta) return;
|
|
189
|
-
if (!meta.variables) meta.variables = {};
|
|
190
|
-
meta.variables[key] = value;
|
|
191
|
-
}
|
|
192
|
-
function removeChatVar(key) {
|
|
193
|
-
const meta = getChatMetadata();
|
|
194
|
-
if (meta && meta.variables) delete meta.variables[key];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* ============ 修改器状态 ============ */
|
|
198
|
-
let current = {
|
|
199
|
-
stat_data: null, // 当前 stat_data(编辑工作区)
|
|
200
|
-
date: null, // date 变量
|
|
201
|
-
calendar: null, // calendar_float_store
|
|
202
|
-
chatVars: null // chat_metadata.variables 副本
|
|
203
|
-
};
|
|
204
|
-
let backup = null; // diff 基线(loadAll/保存时快照;随「保存当前修改」前移)
|
|
205
|
-
let pending = []; // 待提交修改(JSONPatch ops,经「保存当前修改」累积)
|
|
206
|
-
let curTab = ''; // 当前 Tab(section 折叠记忆 key 前缀)
|
|
207
|
-
let scrollMemo = {}; // 各 Tab 滚动记忆:{ [tab]: {top, left} }(上下 .md-body + 左右 .md-tabs)
|
|
208
|
-
let msgBase = null; // 消息基准 stat_data(loadAll 时,用于判断工作区是否有未保存改动)
|
|
209
|
-
let dateBase = null; // 消息基准 date
|
|
210
|
-
let calBase = null; // 消息基准 calendar_float_store
|
|
211
|
-
let log = []; // 操作日志(内存)
|
|
212
|
-
// 持久化日志写入(store 定义在其后,通过函数内引用,运行时已就绪)
|
|
213
|
-
const addLog = (s) => {
|
|
214
|
-
const entry = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + ' ' + s;
|
|
215
|
-
log.unshift(entry);
|
|
216
|
-
if (log.length > 50) log.pop();
|
|
217
|
-
try {
|
|
218
|
-
const raw = store.get(LOG_KEY) || '[]';
|
|
219
|
-
let arr = JSON.parse(raw);
|
|
220
|
-
if (!Array.isArray(arr)) arr = [];
|
|
221
|
-
arr.unshift(entry);
|
|
222
|
-
if (arr.length > 200) arr = arr.slice(0, 200);
|
|
223
|
-
store.set(LOG_KEY, JSON.stringify(arr));
|
|
224
|
-
} catch (e) { /* 忽略 */ }
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// 重新加载全部数据(优先从 chat 消息 variables 读取;失败时回退 MVU API)
|
|
228
|
-
function loadAll() {
|
|
229
|
-
let sd = readLatestVar('stat_data');
|
|
230
|
-
let dt = readLatestVar('date');
|
|
231
|
-
let cal = readLatestVar('calendar_float_store');
|
|
232
|
-
if (!sd || !dt) {
|
|
233
|
-
try {
|
|
234
|
-
const mvu = getMvu();
|
|
235
|
-
if (mvu) {
|
|
236
|
-
let data = null;
|
|
237
|
-
if (typeof mvu.getCurrentMvuData === 'function') data = mvu.getCurrentMvuData();
|
|
238
|
-
if ((!data || typeof data !== 'object') && typeof mvu.getMvuData === 'function') {
|
|
239
|
-
try { data = mvu.getMvuData({ type: 'message' }) || {}; } catch (e2) { /* 忽略 */ }
|
|
240
|
-
}
|
|
241
|
-
if (data && typeof data === 'object') {
|
|
242
|
-
if (!sd && data.stat_data) sd = deepClone(data.stat_data);
|
|
243
|
-
if (!dt && data.date) dt = deepClone(data.date);
|
|
244
|
-
if (!cal && data.calendar_float_store) cal = deepClone(data.calendar_float_store);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
} catch (e) { /* 忽略 */ }
|
|
248
|
-
}
|
|
249
|
-
if (sd) current.stat_data = sd;
|
|
250
|
-
if (dt) current.date = dt;
|
|
251
|
-
if (cal) current.calendar = cal;
|
|
252
|
-
current.chatVars = readChatVars();
|
|
253
|
-
backup = deepClone({ stat_data: current.stat_data, date: current.date });
|
|
254
|
-
msgBase = current.stat_data ? deepClone(current.stat_data) : null;
|
|
255
|
-
dateBase = current.date ? deepClone(current.date) : null;
|
|
256
|
-
calBase = current.calendar ? deepClone(current.calendar) : null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* ============ 配置(持久化到 localStorage,兼容多环境) ============ */
|
|
260
|
-
const LS_KEY = 'md_modifier_v2_cfg';
|
|
261
|
-
const HIS_KEY = 'md_modifier_v2_history';
|
|
262
|
-
const LOG_KEY = 'md_modifier_v2_log';
|
|
263
|
-
let _lsCache = null;
|
|
264
|
-
const store = {
|
|
265
|
-
_init() {
|
|
266
|
-
if (_lsCache) return _lsCache;
|
|
267
|
-
const cands = [];
|
|
268
|
-
if (typeof window !== 'undefined' && window.localStorage) cands.push(window.localStorage);
|
|
269
|
-
if (P.localStorage) cands.push(P.localStorage);
|
|
270
|
-
if (P.parent && P.parent.localStorage) cands.push(P.parent.localStorage);
|
|
271
|
-
if (P.top && P.top.localStorage) cands.push(P.top.localStorage);
|
|
272
|
-
for (const c of cands) {
|
|
273
|
-
try { c.setItem('__md_test__', '1'); c.removeItem('__md_test__'); _lsCache = c; return c; } catch (e) { /* 忽略 */ }
|
|
274
|
-
}
|
|
275
|
-
return null;
|
|
276
|
-
},
|
|
277
|
-
get: k => { try { const v = store._init(); return v ? v.getItem(k) : null; } catch (e) { return null; } },
|
|
278
|
-
set: (k, v) => { try { const s = store._init(); if (s) s.setItem(k, v); } catch (e) { /* 忽略 */ } },
|
|
279
|
-
del: k => { try { const s = store._init(); if (s) s.removeItem(k); } catch (e) { /* 忽略 */ } }
|
|
280
|
-
};
|
|
281
|
-
const DEFAULT_CFG = {
|
|
282
|
-
writeMode: 'body', // 默认正文注入(安全性:正文 > 直接;新手默认正文避免误操作)
|
|
283
|
-
confirmApply: true, // 应用修改前二次确认
|
|
284
|
-
confirmDelete: true, // 删除条目二次确认
|
|
285
|
-
autoSave: true, // 应用后自动触发 saveChat
|
|
286
|
-
syncMvu: true, // 应用后同步 MVU replaceCurrentMvuData
|
|
287
|
-
rememberPos: true, // 记住窗口位置/尺寸
|
|
288
|
-
maxHistory: 20, // 回滚历史条数上限
|
|
289
|
-
winW: 420, winH: 0, posX: null, posY: null
|
|
290
|
-
};
|
|
291
|
-
let cfg = {};
|
|
292
|
-
function loadCfg() {
|
|
293
|
-
cfg = Object.assign({}, DEFAULT_CFG);
|
|
294
|
-
try { const raw = store.get(LS_KEY); if (raw) cfg = Object.assign(cfg, JSON.parse(raw)); } catch (e) { /* 忽略 */ }
|
|
295
|
-
}
|
|
296
|
-
function saveCfg() { store.set(LS_KEY, JSON.stringify(cfg)); }
|
|
297
|
-
|
|
298
|
-
/* ============ 修改历史(回滚) ============ */
|
|
299
|
-
function pushHistory(label) {
|
|
300
|
-
try {
|
|
301
|
-
const raw = store.get(HIS_KEY) || '[]';
|
|
302
|
-
let hist = JSON.parse(raw);
|
|
303
|
-
if (!Array.isArray(hist)) hist = [];
|
|
304
|
-
hist.unshift({
|
|
305
|
-
time: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
306
|
-
label: label || '手动修改',
|
|
307
|
-
stat_data: current.stat_data ? deepClone(current.stat_data) : null,
|
|
308
|
-
date: current.date ? deepClone(current.date) : null
|
|
309
|
-
});
|
|
310
|
-
if (hist.length > (cfg.maxHistory || 20)) hist = hist.slice(0, cfg.maxHistory);
|
|
311
|
-
store.set(HIS_KEY, JSON.stringify(hist));
|
|
312
|
-
addLog('已记录回滚点:' + label);
|
|
313
|
-
} catch (e) { /* 忽略 */ }
|
|
314
|
-
}
|
|
315
|
-
function getHistory() {
|
|
316
|
-
try { const raw = store.get(HIS_KEY) || '[]'; const h = JSON.parse(raw); return Array.isArray(h) ? h : []; }
|
|
317
|
-
catch (e) { return []; }
|
|
318
|
-
}
|
|
319
|
-
function clearHistory() { store.del(HIS_KEY); }
|
|
320
|
-
|
|
321
|
-
/* ============ 正文注入历史 ============ */
|
|
322
|
-
const INJ_KEY = 'md_modifier_v2_inject';
|
|
323
|
-
function pushInjLog(ops) {
|
|
324
|
-
try {
|
|
325
|
-
const raw = store.get(INJ_KEY) || '[]';
|
|
326
|
-
let arr = JSON.parse(raw);
|
|
327
|
-
if (!Array.isArray(arr)) arr = [];
|
|
328
|
-
arr.unshift({
|
|
329
|
-
time: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
330
|
-
count: ops.length,
|
|
331
|
-
ops: ops.map(o => ({
|
|
332
|
-
op: o.op,
|
|
333
|
-
path: o.path,
|
|
334
|
-
value: typeof o.value === 'object' ? JSON.stringify(o.value).slice(0, 120) : String(o.value).slice(0, 120)
|
|
335
|
-
}))
|
|
336
|
-
});
|
|
337
|
-
if (arr.length > 50) arr = arr.slice(0, 50);
|
|
338
|
-
store.set(INJ_KEY, JSON.stringify(arr));
|
|
339
|
-
} catch (e) { /* 忽略 */ }
|
|
340
|
-
}
|
|
341
|
-
function getInjLog() {
|
|
342
|
-
try { const raw = store.get(INJ_KEY) || '[]'; const a = JSON.parse(raw); return Array.isArray(a) ? a : []; }
|
|
343
|
-
catch (e) { return []; }
|
|
344
|
-
}
|
|
345
|
-
function clearInjLog() { store.del(INJ_KEY); }
|
|
346
|
-
|
|
347
|
-
/* ============ 操作日志(持久化,存储读取辅助) ============ */
|
|
348
|
-
function getPersistLog() {
|
|
349
|
-
try { const raw = store.get(LOG_KEY) || '[]'; const a = JSON.parse(raw); return Array.isArray(a) ? a : []; }
|
|
350
|
-
catch (e) { return []; }
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/* ============ 自定义确认弹窗(二次确认) ============ */
|
|
354
|
-
function confirmDialog(title, bodyHtml, okText, cancelText) {
|
|
355
|
-
return new Promise(resolve => {
|
|
356
|
-
const ov = P.document.createElement('div');
|
|
357
|
-
ov.className = 'md-overlay';
|
|
358
|
-
ov.innerHTML = `<div class="md-dialog">
|
|
359
|
-
<h3>${esc(title)}</h3>
|
|
360
|
-
<div class="md-dbody">${bodyHtml}</div>
|
|
361
|
-
<div class="md-dbtns">
|
|
362
|
-
<button class="md-btn" data-act="no">${esc(cancelText || '取消')}</button>
|
|
363
|
-
<button class="md-btn primary" data-act="ok">${esc(okText || '确定')}</button>
|
|
364
|
-
</div></div>`;
|
|
365
|
-
const done = act => { ov.remove(); resolve(act === 'ok'); };
|
|
366
|
-
ov.querySelector('[data-act=no]').addEventListener('click', () => done('no'));
|
|
367
|
-
ov.querySelector('[data-act=ok]').addEventListener('click', () => done('ok'));
|
|
368
|
-
P.document.body.appendChild(ov);
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
// 小白说明弹窗
|
|
372
|
-
function helpDialog(title, html) {
|
|
373
|
-
return confirmDialog(title, html, '知道了', null);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/* ============ 通用 UI 构建 ============ */
|
|
377
|
-
const STYLE_ID = '__md_modifier_style__';
|
|
378
|
-
function injectStyle() {
|
|
379
|
-
if ($('#' + STYLE_ID, P.document)) return;
|
|
380
|
-
const st = P.document.createElement('style');
|
|
381
|
-
st.id = STYLE_ID;
|
|
382
|
-
st.textContent = `
|
|
383
|
-
#md-modifier-root{position:fixed;right:16px;top:16px;z-index:99999;width:420px;max-height:85vh;display:flex;flex-direction:column;border-radius:12px;--md-bg0:rgba(18,20,26,.97);--md-bg1:#1d212c;--md-bg2:#232a3d;--md-line:#2c3140;--md-line2:#3a3f4e;--md-tx:#d8dce6;--md-tx2:#9aa1b5;--md-accent:#ffd27a;--md-ok:#8fd3a8;--md-danger:#ff8fa8;background:var(--md-bg0,rgba(18,20,26,.97));border:1px solid var(--md-line2,#3a3f4e);box-shadow:0 8px 40px rgba(0,0,0,.6);color:var(--md-tx,#d8dce6);font:13px/1.5 "Segoe UI",system-ui,"Microsoft YaHei",sans-serif;user-select:none;overflow:hidden}
|
|
384
|
-
#md-modifier-root *{box-sizing:border-box}
|
|
385
|
-
#md-modifier-root input[type=number]::-webkit-inner-spin-button{opacity:.4}
|
|
386
|
-
#md-modifier-root textarea{resize:vertical}
|
|
387
|
-
#md-modifier-root .md-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,var(--md-bg2,#1c2333),var(--md-bg2,#232a3d));border-bottom:1px solid var(--md-line2,#3a3f4e);cursor:move;flex:0 0 auto}
|
|
388
|
-
#md-modifier-root .md-head .md-title{font-weight:600;color:var(--md-accent,#ffd27a);letter-spacing:1px;font-size:13px;display:flex;align-items:center;gap:6px;flex:1;white-space:nowrap;overflow:hidden}
|
|
389
|
-
#md-modifier-root .md-head .md-title svg{color:var(--md-accent,#ffd27a)}
|
|
390
|
-
#md-modifier-root .md-hbtn{border:none;background:transparent;color:var(--md-tx2,#9aa1b5);cursor:pointer;padding:3px 6px;border-radius:6px;font-size:13px;line-height:1}
|
|
391
|
-
#md-modifier-root .md-hbtn:hover{background:#3a4152;color:#fff}
|
|
392
|
-
#md-modifier-root .md-tabs{display:flex;gap:2px;padding:6px 8px 0;background:var(--md-bg0,#15171f);border-bottom:1px solid var(--md-line,#2c3140);flex:0 0 auto;overflow-x:auto}
|
|
393
|
-
#md-modifier-root .md-tab{border:none;background:transparent;color:var(--md-tx2,#9aa1b5);padding:6px 9px;border-radius:8px 8px 0 0;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:5px;white-space:nowrap}
|
|
394
|
-
#md-modifier-root .md-tab:hover{background:var(--md-bg2,#232838);color:var(--md-tx,#dfe3ee)}
|
|
395
|
-
#md-modifier-root .md-tab.active{background:var(--md-bg2,#232a3d);color:var(--md-accent,#ffd27a);box-shadow:inset 0 2px 0 var(--md-accent,#ffd27a)}
|
|
396
|
-
#md-modifier-root .md-body{flex:1;overflow-y:auto;padding:8px;background:var(--md-bg0,#181b24)}
|
|
397
|
-
#md-modifier-root .md-sec{border:1px solid var(--md-line,#2c3140);border-radius:8px;margin-bottom:8px;overflow:hidden;background:var(--md-bg1,#1d212c)}
|
|
398
|
-
#md-modifier-root .md-sec-title{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--md-bg2,#212636);color:var(--md-tx,#b8c2e0);font-weight:600;font-size:12px;cursor:pointer}
|
|
399
|
-
#md-modifier-root .md-sec-title svg{color:#7d8bb8}
|
|
400
|
-
#md-modifier-root .md-sec-body{padding:8px 10px;display:none}
|
|
401
|
-
#md-modifier-root .md-sec.open .md-sec-body{display:block}
|
|
402
|
-
#md-modifier-root .md-sec-body{overflow-x:auto;overflow-y:visible}
|
|
403
|
-
#md-modifier-root .md-sec-body > .md-row{min-width:280px}
|
|
404
|
-
#md-modifier-root .md-sec-body > .md-grid{grid-template-columns:repeat(2,minmax(260px,1fr));min-width:100%}
|
|
405
|
-
#md-modifier-root .md-row{display:flex;align-items:center;gap:6px;margin-bottom:6px}
|
|
406
|
-
#md-modifier-root .md-row label{width:88px;flex:0 0 auto;color:var(--md-tx2,#9aa1b5);font-size:12px;text-align:right}
|
|
407
|
-
#md-modifier-root .md-row input[type=text],#md-modifier-root .md-row input[type=number]{flex:1;min-width:0;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:4px 8px;font-size:12px;outline:none}
|
|
408
|
-
#md-modifier-root .md-row input:focus{border-color:var(--md-accent,#ffd27a)}
|
|
409
|
-
#md-modifier-root .md-row textarea{width:100%;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:4px 8px;font-size:12px;outline:none;font-family:inherit;min-height:44px}
|
|
410
|
-
#md-modifier-root .md-row select{flex:1;min-width:0;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:4px 8px;font-size:12px;outline:none}
|
|
411
|
-
#md-modifier-root .md-btn{border:1px solid #3a4152;background:var(--md-bg2,#232838);color:var(--md-tx,#c9d1e5);border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;display:inline-flex;align-items:center;gap:4px}
|
|
412
|
-
#md-modifier-root .md-btn:hover{border-color:var(--md-accent,#ffd27a);color:var(--md-accent,#ffd27a)}
|
|
413
|
-
#md-modifier-root .md-btn.primary{background:linear-gradient(90deg,#5b6cff,#7d5bff);border:none;color:#fff;font-weight:600}
|
|
414
|
-
#md-modifier-root .md-btn.danger{background:#3a1f2b;border-color:#7a3b52;color:#ff8fa8}
|
|
415
|
-
#md-modifier-root .md-btn.mini{padding:2px 6px;font-size:11px}
|
|
416
|
-
#md-modifier-root .md-item{display:flex;align-items:center;gap:4px;padding:3px 6px;margin-bottom:4px;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#2c3140);border-radius:6px}
|
|
417
|
-
#md-modifier-root .md-item .md-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--md-tx,#cdd6ea)}
|
|
418
|
-
#md-modifier-root .md-item .md-item-sub{color:var(--md-tx2,#7d88a8);font-size:11px}
|
|
419
|
-
#md-modifier-root .md-item input{flex:1;min-width:0;background:var(--md-bg1,#0e1017);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:4px;padding:2px 6px;font-size:12px;outline:none}
|
|
420
|
-
#md-modifier-root .md-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
|
421
|
-
#md-modifier-root .md-note{color:var(--md-tx2,#7d88a8);font-size:11px;padding:2px 4px;line-height:1.6}
|
|
422
|
-
#md-modifier-root .md-log{font-family:Consolas,monospace;font-size:11px;color:var(--md-tx,#9fd39f);max-height:200px;overflow-y:auto;background:var(--md-bg1,#0e1017);border:1px solid var(--md-line,#2c3140);border-radius:6px;padding:6px}
|
|
423
|
-
#md-modifier-root .md-json{width:100%;min-height:120px;background:var(--md-bg1,#0e1017);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#8fd3a8);border-radius:6px;padding:6px;font:11px/1.5 Consolas,monospace;outline:none}
|
|
424
|
-
#md-modifier-root .md-fab,#md-modifier-fab{position:fixed;right:16px;top:16px;z-index:99999;width:44px;height:44px;border-radius:50%;border:1px solid var(--md-accent,#ffd27a);background:linear-gradient(135deg,var(--md-bg2,#1c2333),var(--md-bg2,#232a3d));color:var(--md-accent,#ffd27a);cursor:pointer;display:none;align-items:center;justify-content:center;box-shadow:0 4px 20px rgba(0,0,0,.5)}
|
|
425
|
-
#md-modifier-root .md-fab:hover,#md-modifier-fab:hover{transform:scale(1.06)}
|
|
426
|
-
#md-modifier-root .md-empty{color:var(--md-tx2,#6b7391);font-size:12px;padding:10px;text-align:center}
|
|
427
|
-
#md-modifier-root .md-chip{display:inline-flex;align-items:center;gap:3px;background:var(--md-bg2,#232a3d);border:1px solid #3a4152;border-radius:20px;padding:1px 8px;font-size:11px;color:var(--md-tx,#b8c2e0);margin:2px}
|
|
428
|
-
#md-modifier-root::-webkit-scrollbar,#md-modifier-root *::-webkit-scrollbar{width:6px;height:6px}
|
|
429
|
-
#md-modifier-root::-webkit-scrollbar-thumb,#md-modifier-root *::-webkit-scrollbar-thumb{background:#3a4152;border-radius:3px}
|
|
430
|
-
#md-modifier-root .md-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;opacity:.5;transition:opacity .2s;z-index:5}
|
|
431
|
-
#md-modifier-root .md-resize:hover{opacity:1}
|
|
432
|
-
#md-modifier-root .md-resize::before{content:"";position:absolute;right:3px;bottom:3px;width:8px;height:8px;border-right:2px solid var(--md-accent,#ffd27a);border-bottom:2px solid var(--md-accent,#ffd27a)}
|
|
433
|
-
#md-modifier-root .md-help{cursor:help;color:#7d8bb8;border:1px solid #3a4152;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;line-height:1;background:var(--md-bg2,#212636);flex:0 0 auto;margin-left:4px}
|
|
434
|
-
#md-modifier-root .md-help:hover{color:var(--md-accent,#ffd27a);border-color:var(--md-accent,#ffd27a)}
|
|
435
|
-
#md-modifier-root .md-helpbox{display:none;margin-top:6px;padding:8px 10px;background:var(--md-bg1,#10131c);border:1px solid #3a4152;border-left:3px solid var(--md-accent,#ffd27a);border-radius:6px;font-size:12px;line-height:1.8;color:var(--md-tx,#c6cede)}
|
|
436
|
-
#md-modifier-root .md-helpbox.show{display:block}
|
|
437
|
-
#md-modifier-root .md-helpbox b{color:var(--md-accent,#ffd27a)}
|
|
438
|
-
#md-modifier-root .md-helpbox code{background:var(--md-bg1,#0e1017);border:1px solid var(--md-line,#333a4d);border-radius:4px;padding:0 4px;font:11px Consolas,monospace;color:var(--md-tx,#8fd3a8)}
|
|
439
|
-
.md-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100000;display:flex;align-items:center;justify-content:center}
|
|
440
|
-
.md-dialog{background:var(--md-bg1,#1d212c);border:1px solid var(--md-accent,#ffd27a);border-radius:10px;width:420px;max-width:90vw;max-height:70vh;overflow:auto;padding:14px;box-shadow:0 8px 40px rgba(0,0,0,.7);font:13px/1.5 "Segoe UI",system-ui,"Microsoft YaHei",sans-serif;color:var(--md-tx,#d8dce6)}
|
|
441
|
-
.md-dialog h3{margin:0 0 8px;color:var(--md-accent,#ffd27a);font-size:14px}
|
|
442
|
-
.md-dialog .md-dbody{color:var(--md-tx,#cdd6ea);font-size:12px;line-height:1.7;max-height:40vh;overflow:auto;margin-bottom:10px}
|
|
443
|
-
.md-dialog .md-dbody ul{margin:4px 0;padding-left:18px}
|
|
444
|
-
.md-dialog .md-dbtns{display:flex;gap:8px;justify-content:flex-end}
|
|
445
|
-
.md-dialog .md-btn{border:1px solid #3a4152;background:var(--md-bg2,#232838);color:var(--md-tx,#c9d1e5);border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;display:inline-flex;align-items:center;gap:4px}
|
|
446
|
-
.md-dialog .md-btn:hover{border-color:var(--md-accent,#ffd27a);color:var(--md-accent,#ffd27a)}
|
|
447
|
-
.md-dialog .md-btn.primary{background:linear-gradient(90deg,#5b6cff,#7d5bff);border:none;color:#fff;font-weight:600}
|
|
448
|
-
.md-dialog .md-btn.danger{background:#3a1f2b;border-color:#7a3b52;color:#ff8fa8}
|
|
449
|
-
.md-dialog b{color:var(--md-accent,#ffd27a)}
|
|
450
|
-
.md-dialog .md-note{color:var(--md-tx2,#7d88a8);font-size:11px}
|
|
451
|
-
#md-modifier-root .md-check{display:flex;align-items:center;gap:8px;margin-bottom:8px;color:var(--md-tx,#cdd6ea);font-size:12px;cursor:pointer}
|
|
452
|
-
#md-modifier-root .md-check input{width:auto;flex:0 0 auto}
|
|
453
|
-
#md-modifier-root .md-cfgrow{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px dashed var(--md-line,#2c3140);font-size:12px}
|
|
454
|
-
#md-modifier-root .md-cfgrow label{flex:1;color:var(--md-tx,#b8c2e0)}
|
|
455
|
-
#md-modifier-root .md-cfgrow select,#md-modifier-root .md-cfgrow input[type=number]{background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:3px 8px;font-size:12px;outline:none;width:auto}
|
|
456
|
-
#md-modifier-root .md-hist{display:flex;align-items:center;gap:6px;padding:4px 6px;margin-bottom:4px;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#2c3140);border-radius:6px;font-size:11px;color:var(--md-tx2,#9aa1b5)}
|
|
457
|
-
#md-modifier-root .md-hist .md-hist-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
458
|
-
/* 触屏拖拽/缩放:禁用默认手势做元素拖动,释放流畅触控 */
|
|
459
|
-
#md-modifier-root .md-head,#md-modifier-fab,#md-modifier-root .md-resize{touch-action:none;-webkit-user-select:none;user-select:none}
|
|
460
|
-
#md-modifier-root{max-width:100vw}
|
|
461
|
-
#md-modifier-root .md-tabs{-webkit-overflow-scrolling:touch}
|
|
462
|
-
/* 全屏模式(md-max 按钮切换):铺满视口,隐藏缩放手柄与固定定位 */
|
|
463
|
-
#md-modifier-root.md-fullscreen{left:0!important;top:0!important;right:0!important;width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0;border:none}
|
|
464
|
-
#md-modifier-root.md-fullscreen .md-resize{display:none}
|
|
465
|
-
/* 移动端 / 窄屏:窗口铺满视口,单列布局,隐藏缩放手柄(用全屏/还原按钮代替) */
|
|
466
|
-
@media (max-width:768px){
|
|
467
|
-
#md-modifier-root{left:0!important;top:0!important;right:0!important;width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0;border:none}
|
|
468
|
-
#md-modifier-root .md-resize{display:none}
|
|
469
|
-
#md-modifier-root .md-grid{grid-template-columns:1fr!important}
|
|
470
|
-
#md-modifier-root .md-body{padding:6px}
|
|
471
|
-
#md-modifier-root .md-sec-body > .md-row{min-width:0}
|
|
472
|
-
#md-modifier-root .md-sec-body > .md-grid{min-width:0}
|
|
473
|
-
#md-modifier-fab{right:12px;top:12px;width:52px;height:52px}
|
|
474
|
-
#md-modifier-root .md-row label{width:76px}
|
|
475
|
-
#md-modifier-root .md-head{padding:6px 8px}
|
|
476
|
-
}
|
|
477
|
-
`;
|
|
478
|
-
P.document.head.appendChild(st);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/* 主面板骨架 */
|
|
482
|
-
let root = null;
|
|
483
|
-
const VER = 'v2.13';
|
|
484
|
-
function buildPanel() {
|
|
485
|
-
root = P.document.createElement('div');
|
|
486
|
-
root.id = 'md-modifier-root';
|
|
487
|
-
root.innerHTML = `
|
|
488
|
-
<div class="md-head" id="md-head">
|
|
489
|
-
<div class="md-title">${ICONS.wand} 命定之诗 · 全量修改器 <span style="font-size:10px;color:#7d8bb8">${VER}</span></div>
|
|
490
|
-
<button class="md-hbtn" id="md-refresh-head" title="刷新快照">⟳</button>
|
|
491
|
-
<button class="md-hbtn" id="md-max" title="全屏/还原">⛶</button>
|
|
492
|
-
<button class="md-hbtn" id="md-min" title="最小化">—</button>
|
|
493
|
-
<button class="md-hbtn" id="md-close" title="关闭">✕</button>
|
|
494
|
-
</div>
|
|
495
|
-
<div class="md-tabs">
|
|
496
|
-
<button class="md-tab active" data-tab="hero">${ICONS.user} 主角</button>
|
|
497
|
-
<button class="md-tab" data-tab="npc">${ICONS.group} NPC</button>
|
|
498
|
-
<button class="md-tab" data-tab="world">${ICONS.globe} 世界</button>
|
|
499
|
-
<button class="md-tab" data-tab="cal">${ICONS.calendar} 日历</button>
|
|
500
|
-
<button class="md-tab" data-tab="start">${ICONS.spark} 开局</button>
|
|
501
|
-
<button class="md-tab" data-tab="global">${ICONS.palette} 全局</button>
|
|
502
|
-
<button class="md-tab" data-tab="ops">${ICONS.terminal} 操作</button>
|
|
503
|
-
<button class="md-tab" data-tab="parse">${ICONS.edit} 变量解读</button>
|
|
504
|
-
<button class="md-tab" data-tab="settings">${ICONS.gear} 设置</button>
|
|
505
|
-
<button class="md-tab" data-tab="about">${ICONS.question} 关于</button>
|
|
506
|
-
</div>
|
|
507
|
-
<div class="md-body" id="md-body"></div>
|
|
508
|
-
<div class="md-resize" id="md-resize" title="拖动调整窗口大小"></div>`;
|
|
509
|
-
P.document.body.appendChild(root);
|
|
510
|
-
|
|
511
|
-
const fab = P.document.createElement('div');
|
|
512
|
-
fab.id = 'md-modifier-fab';
|
|
513
|
-
fab.className = 'md-fab';
|
|
514
|
-
fab.innerHTML = ICONS.wand;
|
|
515
|
-
fab.title = '打开命定之诗修改器';
|
|
516
|
-
fab.style.display = 'none';
|
|
517
|
-
P.document.body.appendChild(fab);
|
|
518
|
-
// 悬浮球:点击开面板(拖拽后抑制)+ 拖拽(修复:此前 fab 仅可点击不可拖动)+ 拖 fab 时窗口跟随同步
|
|
519
|
-
fab.addEventListener('click', e => {
|
|
520
|
-
if (fab._moved) { fab._moved = false; e.preventDefault(); e.stopPropagation(); return; }
|
|
521
|
-
fab.style.display = 'none'; root.style.display = 'flex'; applyTheme(); render();
|
|
522
|
-
});
|
|
523
|
-
fab.addEventListener('pointerdown', e => {
|
|
524
|
-
if (e.button !== 0 && e.pointerType === 'mouse') return;
|
|
525
|
-
e.preventDefault();
|
|
526
|
-
fab._moved = false; // 每次按下先复位(防上次拖拽 _moved 残留吞掉本次点击)
|
|
527
|
-
const sx = e.clientX, sy = e.clientY;
|
|
528
|
-
const ol = fab.offsetLeft || 16, ot = fab.offsetTop || 16;
|
|
529
|
-
let moved = false;
|
|
530
|
-
try { fab.setPointerCapture(e.pointerId); } catch (e2) { }
|
|
531
|
-
const move = ev => {
|
|
532
|
-
const dx = ev.clientX - sx, dy = ev.clientY - sy;
|
|
533
|
-
if (!moved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return; // 抖动阈值:4px 内视为点击
|
|
534
|
-
moved = true;
|
|
535
|
-
const x = Math.max(0, Math.min((P.innerWidth || window.innerWidth) - 60, ol + dx));
|
|
536
|
-
const y = Math.max(0, Math.min((P.innerHeight || window.innerHeight) - 60, ot + dy));
|
|
537
|
-
fab.style.right = 'auto';
|
|
538
|
-
fab.style.left = x + 'px';
|
|
539
|
-
fab.style.top = y + 'px';
|
|
540
|
-
};
|
|
541
|
-
const up = () => {
|
|
542
|
-
P.document.removeEventListener('pointermove', move);
|
|
543
|
-
P.document.removeEventListener('pointerup', up);
|
|
544
|
-
if (!moved) return; // 未拖动(点击):不改位置、不打开面板(由 click 处理)
|
|
545
|
-
fab._moved = true; // 抑制本次 click 打开面板
|
|
546
|
-
if (cfg.rememberPos) {
|
|
547
|
-
const l = parseInt(fab.style.left || '0', 10);
|
|
548
|
-
const t = parseInt(fab.style.top || '0', 10);
|
|
549
|
-
cfg.posX = Number.isNaN(l) ? null : l;
|
|
550
|
-
cfg.posY = Number.isNaN(t) ? null : t;
|
|
551
|
-
saveCfg();
|
|
552
|
-
}
|
|
553
|
-
// 窗口跟随悬浮球(仅拖动后;未拖动保持 CSS 默认位置)
|
|
554
|
-
root.style.right = 'auto';
|
|
555
|
-
root.style.left = (Number.isNaN(parseInt(fab.style.left || '0', 10)) ? 0 : parseInt(fab.style.left || '0', 10)) + 'px';
|
|
556
|
-
root.style.top = (Number.isNaN(parseInt(fab.style.top || '0', 10)) ? 0 : parseInt(fab.style.top || '0', 10)) + 'px';
|
|
557
|
-
};
|
|
558
|
-
P.document.addEventListener('pointermove', move);
|
|
559
|
-
P.document.addEventListener('pointerup', up);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// 拖拽(记忆位置)——pointer 事件兼容触屏与鼠标
|
|
563
|
-
const head = $('#md-head', root);
|
|
564
|
-
let dragging = false, dragMoved = false, ox = 0, oy = 0, startCX = 0, startCY = 0;
|
|
565
|
-
head.addEventListener('pointerdown', e => {
|
|
566
|
-
if (e.target.closest('button')) return;
|
|
567
|
-
dragging = true; dragMoved = false; startCX = e.clientX; startCY = e.clientY;
|
|
568
|
-
ox = e.clientX - root.offsetLeft; oy = e.clientY - root.offsetTop;
|
|
569
|
-
try { head.setPointerCapture(e.pointerId); } catch (e2) { }
|
|
570
|
-
});
|
|
571
|
-
P.document.addEventListener('pointermove', e => {
|
|
572
|
-
if (!dragging) return;
|
|
573
|
-
if (!dragMoved && Math.abs(e.clientX - startCX) < 4 && Math.abs(e.clientY - startCY) < 4) return; // 抖动阈值(与 fab 对齐)
|
|
574
|
-
dragMoved = true;
|
|
575
|
-
const x = Math.max(0, Math.min((P.innerWidth || window.innerWidth) - 60, e.clientX - ox));
|
|
576
|
-
const y = Math.max(0, Math.min((P.innerHeight || window.innerHeight) - 60, e.clientY - oy));
|
|
577
|
-
root.style.right = 'auto'; root.style.top = y + 'px'; root.style.left = x + 'px';
|
|
578
|
-
});
|
|
579
|
-
P.document.addEventListener('pointerup', () => {
|
|
580
|
-
if (!dragging) return;
|
|
581
|
-
dragging = false;
|
|
582
|
-
if (!dragMoved) return; // 仅拖动过才保存位置/同步悬浮球(单击标题栏不甩球)
|
|
583
|
-
if (cfg.rememberPos) {
|
|
584
|
-
const l = parseInt(root.style.left || '0', 10);
|
|
585
|
-
const t = parseInt(root.style.top || '0', 10);
|
|
586
|
-
cfg.posX = Number.isNaN(l) ? null : l;
|
|
587
|
-
cfg.posY = Number.isNaN(t) ? null : t;
|
|
588
|
-
cfg.winW = root.offsetWidth || cfg.winW;
|
|
589
|
-
cfg.winH = root.offsetHeight || cfg.winH;
|
|
590
|
-
saveCfg();
|
|
591
|
-
}
|
|
592
|
-
// 悬浮球跟随窗口(收起后悬浮球出现在窗口位置)
|
|
593
|
-
fab.style.right = 'auto';
|
|
594
|
-
fab.style.left = (Number.isNaN(parseInt(root.style.left || '0', 10)) ? 0 : parseInt(root.style.left || '0', 10)) + 'px';
|
|
595
|
-
fab.style.top = (Number.isNaN(parseInt(root.style.top || '0', 10)) ? 0 : parseInt(root.style.top || '0', 10)) + 'px';
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// 窗口大小拖动(右下角手柄)——pointer 事件兼容触屏与鼠标
|
|
599
|
-
const rz = $('#md-resize', root);
|
|
600
|
-
let resizing = false, rx = 0, ry = 0, rw = 0, rh = 0;
|
|
601
|
-
rz.addEventListener('pointerdown', e => {
|
|
602
|
-
e.preventDefault(); e.stopPropagation();
|
|
603
|
-
resizing = true; rx = e.clientX; ry = e.clientY;
|
|
604
|
-
rw = root.offsetWidth; rh = root.offsetHeight;
|
|
605
|
-
try { rz.setPointerCapture(e.pointerId); } catch (e2) { }
|
|
606
|
-
});
|
|
607
|
-
P.document.addEventListener('pointermove', e => {
|
|
608
|
-
if (!resizing) return;
|
|
609
|
-
const nw = Math.max(300, Math.min((P.innerWidth || 1920) - 40, rw + (e.clientX - rx)));
|
|
610
|
-
const nh = Math.max(220, Math.min((P.innerHeight || 1080) - 40, rh + (e.clientY - ry)));
|
|
611
|
-
root.style.width = nw + 'px';
|
|
612
|
-
root.style.height = nh + 'px';
|
|
613
|
-
});
|
|
614
|
-
P.document.addEventListener('pointerup', () => {
|
|
615
|
-
if (!resizing) return;
|
|
616
|
-
resizing = false;
|
|
617
|
-
if (cfg.rememberPos) {
|
|
618
|
-
cfg.winW = root.offsetWidth; cfg.winH = root.offsetHeight;
|
|
619
|
-
saveCfg();
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// 应用记忆的位置与尺寸
|
|
624
|
-
if (cfg.rememberPos && cfg.winW) {
|
|
625
|
-
root.style.width = cfg.winW + 'px';
|
|
626
|
-
if (cfg.winH) root.style.height = cfg.winH + 'px';
|
|
627
|
-
if (cfg.posX !== null && cfg.posY !== null) {
|
|
628
|
-
root.style.right = 'auto'; root.style.left = cfg.posX + 'px'; root.style.top = cfg.posY + 'px';
|
|
629
|
-
// 悬浮球同步恢复位置(双位置一致)
|
|
630
|
-
fab.style.right = 'auto';
|
|
631
|
-
fab.style.left = cfg.posX + 'px';
|
|
632
|
-
fab.style.top = cfg.posY + 'px';
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
$('#md-min', root).addEventListener('click', () => { root.style.display = 'none'; fab.style.display = 'flex'; });
|
|
637
|
-
$('#md-close', root).addEventListener('click', () => { root.style.display = 'none'; fab.style.display = 'flex'; });
|
|
638
|
-
// 全屏/还原(移动端缩放手柄隐藏时用;任何设备可用)
|
|
639
|
-
$('#md-max', root).addEventListener('click', () => {
|
|
640
|
-
const fs = root.classList.toggle('md-fullscreen');
|
|
641
|
-
if (fs) {
|
|
642
|
-
// 进入全屏:备份当前尺寸(光标下剩余空间)
|
|
643
|
-
if (cfg.rememberPos) { cfg.winW = root.offsetWidth || cfg.winW; cfg.winH = root.offsetHeight || cfg.winH; saveCfg(); }
|
|
644
|
-
} else {
|
|
645
|
-
// 退出全屏:恢复记忆尺寸(若 rememberPos 且已记录)
|
|
646
|
-
if (cfg.rememberPos && cfg.winW) { root.style.width = cfg.winW + 'px'; if (cfg.winH) root.style.height = cfg.winH + 'px'; }
|
|
647
|
-
}
|
|
648
|
-
toast(fs ? '已切换全屏(标题栏 ⛶ 可还原)' : '已还原窗口尺寸', 'info');
|
|
649
|
-
});
|
|
650
|
-
// 标题栏刷新按钮:实时重载当前快照变量
|
|
651
|
-
$('#md-refresh-head', root).addEventListener('click', () => {
|
|
652
|
-
loadAll();
|
|
653
|
-
render();
|
|
654
|
-
toast('快照已刷新', 'success');
|
|
655
|
-
addLog('手动刷新快照');
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
// Tab 切换
|
|
659
|
-
$$('.md-tab', root).forEach(t => t.addEventListener('click', () => {
|
|
660
|
-
$$('.md-tab', root).forEach(x => x.classList.toggle('active', x === t));
|
|
661
|
-
render();
|
|
662
|
-
}));
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function render() {
|
|
666
|
-
const tab = $('.md-tab.active', root).dataset.tab;
|
|
667
|
-
const body = $('#md-body', root);
|
|
668
|
-
const tabsEl = $('.md-tabs', root);
|
|
669
|
-
// 保存上一位 tab 的滚动位置(body 目前还是旧内容)
|
|
670
|
-
if (curTab) { scrollMemo[curTab] = { top: body.scrollTop || 0, left: (tabsEl && tabsEl.scrollLeft) || 0 }; }
|
|
671
|
-
curTab = tab;
|
|
672
|
-
body.innerHTML = '';
|
|
673
|
-
if (tab === 'settings') { renderSettings(body); renderUi(body); renderHelp(body); }
|
|
674
|
-
else if (tab === 'parse') { renderParse(body); }
|
|
675
|
-
else if (tab === 'about') { renderAbout(body); }
|
|
676
|
-
else if (tab === 'start') { renderStart(body); }
|
|
677
|
-
else if (!current.stat_data) { body.innerHTML = '<div class="md-empty">未找到 stat_data 变量。<br>请先进入一个命定之诗聊天,再刷新本面板。</div>'; }
|
|
678
|
-
else if (tab === 'ops') { const f = renderOps; f(body); renderArchive(body); }
|
|
679
|
-
else ({ hero: renderHero, npc: renderNpc, world: renderWorld, cal: renderCal, global: renderGlobal })[tab](body);
|
|
680
|
-
// 恢复本 tab 的滚动位置(上下 .md-body + 左右 .md-tabs)
|
|
681
|
-
const m = scrollMemo[tab];
|
|
682
|
-
if (m) { try { body.scrollTop = m.top; if (tabsEl) tabsEl.scrollLeft = m.left; } catch (e) { } }
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/* 区块组件(折叠状态记忆:__mds_fold_<tab>_<key>;用户折叠后重渲染/重开保持) */
|
|
686
|
-
function section(title, icon, bodyHtml, open = true, key = null) {
|
|
687
|
-
const k = curTab + '_' + (key || title);
|
|
688
|
-
let mem = null;
|
|
689
|
-
try { mem = store.get('__mds_fold_' + k); } catch (e) { }
|
|
690
|
-
const closed = mem === '1' || (mem === null && !open);
|
|
691
|
-
return `<div class="md-sec ${closed ? '' : 'open'}"><div class="md-sec-title" data-fk="${esc(k)}">${ICONS[icon] || ''} ${esc(title)}</div><div class="md-sec-body">${bodyHtml}</div></div>`;
|
|
692
|
-
}
|
|
693
|
-
// 统一折叠绑定(点击切换 open 并写入记忆)
|
|
694
|
-
// 幂等:同一 .md-sec-title 只绑一次(防 renderUi/renderHelp 追加后重复 bindFoldCommon 造成偶数次 toggle 抵消)
|
|
695
|
-
function bindFoldCommon(body) {
|
|
696
|
-
$$('.md-sec-title', body).forEach(t => {
|
|
697
|
-
if (t._foldBound) return;
|
|
698
|
-
t._foldBound = true;
|
|
699
|
-
t.addEventListener('click', () => {
|
|
700
|
-
const secEl = t.parentElement;
|
|
701
|
-
secEl.classList.toggle('open');
|
|
702
|
-
const fk = t.getAttribute('data-fk');
|
|
703
|
-
if (fk) { try { store.set('__mds_fold_' + fk, secEl.classList.contains('open') ? '0' : '1'); } catch (e) { } }
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
// 带问号帮助按钮的区块:点击展开内联帮助说明
|
|
708
|
-
function sectionHelp(title, icon, helpHtml, bodyHtml, open = true) {
|
|
709
|
-
return `<div class="md-sec ${open ? 'open' : ''}">
|
|
710
|
-
<div class="md-sec-title">${ICONS[icon] || ''} ${esc(title)}<span class="md-help" title="查看说明">?</span></div>
|
|
711
|
-
<div class="md-helpbox">${helpHtml}</div>
|
|
712
|
-
<div class="md-sec-body">${bodyHtml}</div>
|
|
713
|
-
</div>`;
|
|
714
|
-
}
|
|
715
|
-
function row(label, inner, w = 88) {
|
|
716
|
-
return `<div class="md-row"><label style="width:${w}px">${esc(label)}</label>${inner}</div>`;
|
|
717
|
-
}
|
|
718
|
-
function textInput(id, val) { return `<input type="text" id="md-${id}" value="${esc(val)}">`; }
|
|
719
|
-
function numInput(id, val) { return `<input type="number" id="md-${id}" value="${esc(val ?? '')}">`; }
|
|
720
|
-
|
|
721
|
-
/* ============ Tab1 主角 ============ */
|
|
722
|
-
function renderHero(body) {
|
|
723
|
-
const d = current.stat_data;
|
|
724
|
-
const hero = d['主角'] || {};
|
|
725
|
-
const G = d['世界'] || {};
|
|
726
|
-
const isNew = isResObj(hero['生命值']) || isResObj(hero['法力值']) || isResObj(hero['体力值']); // v4.3.1 新版结构
|
|
727
|
-
let html = '';
|
|
728
|
-
html += section('基础属性', 'user', `
|
|
729
|
-
${row('种族', textInput('h_race', hero['种族'] || ''))}
|
|
730
|
-
${row('生命层级', textInput('h_life', hero['生命层级'] || ''))}
|
|
731
|
-
<div class="md-grid">
|
|
732
|
-
${row('等级', numInput('h_level', hero['等级'] ?? 1))}
|
|
733
|
-
${isNew ? '' : row('冒险者等级', textInput('h_adv', hero['冒险者等级'] || ''))}
|
|
734
|
-
</div>
|
|
735
|
-
<div class="md-grid">
|
|
736
|
-
${row('累计经验', numInput('h_exp', hero['累计经验值'] ?? 0))}
|
|
737
|
-
${row('升级所需', numInput('h_expneed', hero['升级所需经验'] ?? 120))}
|
|
738
|
-
</div>
|
|
739
|
-
${row('属性点', numInput('h_ap', hero['属性点'] ?? 0))}
|
|
740
|
-
${row('身份(逗号分隔)', textInput('h_ids', (hero['身份'] || []).join(',')))}
|
|
741
|
-
${row('职业(逗号分隔)', textInput('h_occ', (hero['职业'] || []).join(',')))}
|
|
742
|
-
`);
|
|
743
|
-
html += section('五维属性', 'spark', `
|
|
744
|
-
<div class="md-grid">
|
|
745
|
-
${row('力量', numInput('h_str', (hero['属性']||{})['力量'] ?? 0))}
|
|
746
|
-
${row('敏捷', numInput('h_agi', (hero['属性']||{})['敏捷'] ?? 0))}
|
|
747
|
-
${row('体质', numInput('h_con', (hero['属性']||{})['体质'] ?? 0))}
|
|
748
|
-
${row('智力', numInput('h_int', (hero['属性']||{})['智力'] ?? 0))}
|
|
749
|
-
${row('精神', numInput('h_wis', (hero['属性']||{})['精神'] ?? 0))}
|
|
750
|
-
</div>
|
|
751
|
-
`);
|
|
752
|
-
html += section('资源与货币', 'coin', `
|
|
753
|
-
${renderHeroRes(hero)}
|
|
754
|
-
<div class="md-grid">
|
|
755
|
-
${row('金钱', numInput('h_money', hero['金钱'] ?? 0))}
|
|
756
|
-
${row('命运点数', numInput('h_fp', d['命运点数'] ?? 0))}
|
|
757
|
-
</div>
|
|
758
|
-
`);
|
|
759
|
-
html += section('状态效果', 'heart', renderBuffList(hero, 'hero'), false);
|
|
760
|
-
html += section('背包', 'bag', renderItemList(hero, '背包', 'hero_bag', '物品'), false);
|
|
761
|
-
html += section('装备', 'shield', renderItemList(hero, '装备', 'hero_eq', '装备'), false);
|
|
762
|
-
html += section('技能', 'sword', renderItemList(hero, '技能', 'hero_sk', '技能'), false);
|
|
763
|
-
html += section('资产', 'coin', renderAssetList(hero, 'hero'), false);
|
|
764
|
-
html += section('登神长阶', 'spark', renderAscension(hero), false);
|
|
765
|
-
html += `${applyBarHtml()}`;
|
|
766
|
-
body.innerHTML = html;
|
|
767
|
-
|
|
768
|
-
// 区块折叠
|
|
769
|
-
bindFoldCommon(body);
|
|
770
|
-
|
|
771
|
-
// 数据绑定:改动直接落到工作区
|
|
772
|
-
bindHero(body, hero, d);
|
|
773
|
-
bindItemList(body, hero, '背包', 'hero_bag', '物品');
|
|
774
|
-
bindItemList(body, hero, '装备', 'hero_eq', '装备');
|
|
775
|
-
bindItemList(body, hero, '技能', 'hero_sk', '技能');
|
|
776
|
-
bindBuffs(body, hero);
|
|
777
|
-
bindAscension(body, hero);
|
|
778
|
-
bindAssetList(body, hero, 'hero'); // v4.3.1 资产
|
|
779
|
-
|
|
780
|
-
bindApplyBar(body);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function bindHero(body, hero, d) {
|
|
784
|
-
const bind = (id, fn) => {
|
|
785
|
-
const el = $('#md-' + id, body);
|
|
786
|
-
if (el) el.addEventListener('change', () => fn(el));
|
|
787
|
-
};
|
|
788
|
-
bind('h_race', e => hero['种族'] = e.value);
|
|
789
|
-
bind('h_life', e => hero['生命层级'] = e.value);
|
|
790
|
-
bind('h_level', e => hero['等级'] = Math.max(1, Math.min(25, num(e.value))));
|
|
791
|
-
bind('h_adv', e => hero['冒险者等级'] = e.value);
|
|
792
|
-
bind('h_exp', e => hero['累计经验值'] = Math.max(0, num(e.value)));
|
|
793
|
-
bind('h_expneed', e => hero['升级所需经验'] = num(e.value));
|
|
794
|
-
bind('h_ap', e => hero['属性点'] = Math.max(0, num(e.value)));
|
|
795
|
-
bind('h_ids', e => hero['身份'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean));
|
|
796
|
-
bind('h_occ', e => hero['职业'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean));
|
|
797
|
-
bind('h_str', e => { if (!hero['属性']) hero['属性'] = {}; hero['属性']['力量'] = num(e.value); });
|
|
798
|
-
bind('h_agi', e => { if (!hero['属性']) hero['属性'] = {}; hero['属性']['敏捷'] = num(e.value); });
|
|
799
|
-
bind('h_con', e => { if (!hero['属性']) hero['属性'] = {}; hero['属性']['体质'] = num(e.value); });
|
|
800
|
-
bind('h_int', e => { if (!hero['属性']) hero['属性'] = {}; hero['属性']['智力'] = num(e.value); });
|
|
801
|
-
bind('h_wis', e => { if (!hero['属性']) hero['属性'] = {}; hero['属性']['精神'] = num(e.value); });
|
|
802
|
-
bind('h_money', e => hero['金钱'] = num(e.value));
|
|
803
|
-
bind('h_fp', e => d['命运点数'] = num(e.value));
|
|
804
|
-
bindRes(body, hero, 'h'); // v4.3.1 资源适配:当前/基础(只读)/额外
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/* v4.3.1 资源区块统一渲染与绑定(主角 prefix='h',NPC prefix='n') */
|
|
808
|
-
const RES_KEYS = [['生命值', 'hp'], ['法力值', 'mp'], ['体力值', 'sp']];
|
|
809
|
-
// 渲染资源行:新版显示 当前/基础(只读)/额外/总(只读),旧版显示 当前/上限
|
|
810
|
-
function renderRes(owner, prefix) {
|
|
811
|
-
const isNew = isResObj(owner['生命值']) || isResObj(owner['法力值']) || isResObj(owner['体力值']);
|
|
812
|
-
let rows = '';
|
|
813
|
-
for (const [cn, en] of RES_KEYS) {
|
|
814
|
-
const v = owner[cn];
|
|
815
|
-
if (isNew) {
|
|
816
|
-
const base = resBase(v), extra = resExtra(v), total = resTotal(v, null);
|
|
817
|
-
rows += row(cn, numInput(prefix + '_' + en, resCur(v))) +
|
|
818
|
-
row(cn + '·基础', `<input type="number" id="md-${prefix}_${en}base" value="${esc(base)}" readonly style="width:88px;background:var(--md-bg2,#1a1d27);color:var(--md-dim,#9aa4b2)">`) +
|
|
819
|
-
row(cn + '·额外', numInput(prefix + '_' + en + 'extra', extra)) +
|
|
820
|
-
`<div class="md-note" style="grid-column:1/-1">${cn}总上限 ${esc(total)}(基础只读,额外随装备/状态效果变化)</div>`;
|
|
821
|
-
} else {
|
|
822
|
-
rows += row(cn, numInput(prefix + '_' + en, resCur(v))) +
|
|
823
|
-
row(cn + '上限', numInput(prefix + '_' + en + 'max', owner[cn + '上限'] ?? 0));
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return `<div class="md-grid">${rows}</div>`;
|
|
827
|
-
}
|
|
828
|
-
function renderHeroRes(hero) { return renderRes(hero, 'h'); }
|
|
829
|
-
// 绑定资源输入:当前值 + 额外上限(新版);旧版绑 当前 + 旧上限键
|
|
830
|
-
function bindRes(body, owner, prefix) {
|
|
831
|
-
const bind = (id, fn) => { const el = $('#md-' + id, body); if (el) el.addEventListener('change', () => fn(el)); };
|
|
832
|
-
for (const [cn, en] of RES_KEYS) {
|
|
833
|
-
bind(prefix + '_' + en, e => setResCur(owner, cn, num(e.value)));
|
|
834
|
-
bind(prefix + '_' + en + 'extra', e => setResExtra(owner, cn, num(e.value)));
|
|
835
|
-
// 旧版:绑定旧上限键(若该字段为旧版单值结构)
|
|
836
|
-
bind(prefix + '_' + en + 'max', e => setResExtra(owner, cn, num(e.value)));
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/* 通用物品列表(背包/装备/技能)渲染与绑定 */
|
|
841
|
-
function renderItemList(owner, key, prefix, kind) {
|
|
842
|
-
const list = owner[key] || {};
|
|
843
|
-
let items = Object.entries(list).map(([n, v]) => `<div class="md-item">
|
|
844
|
-
<span class="md-item-name" data-item="${esc(n)}" style="cursor:pointer">${esc(n)}</span>
|
|
845
|
-
<span class="md-item-sub">${esc(v['品质'] || '')}${v['数量'] ? ' ×' + v['数量'] : ''}</span>
|
|
846
|
-
<button class="md-btn mini danger" data-del-item="${esc(n)}">${ICONS.trash}</button>
|
|
847
|
-
</div>`).join('');
|
|
848
|
-
if (!items) items = '<div class="md-empty">暂无' + kind + '</div>';
|
|
849
|
-
return `
|
|
850
|
-
<div id="${prefix}-list">${items}</div>
|
|
851
|
-
<div class="md-row" style="margin-top:6px">
|
|
852
|
-
<input type="text" id="${prefix}-newname" placeholder="新${kind}名称">
|
|
853
|
-
<button class="md-btn mini primary" id="${prefix}-add">${ICONS.plus} 添加</button>
|
|
854
|
-
</div>
|
|
855
|
-
`;
|
|
856
|
-
}
|
|
857
|
-
const ITEM_FIELDS = {
|
|
858
|
-
'背包': ['品质', '类型', '数量', '标签', '效果', '描述'],
|
|
859
|
-
'装备': ['品质', '类型', '位置', '标签', '效果', '描述'],
|
|
860
|
-
'技能': ['品质', '类型', '消耗', '标签', '效果', '描述']
|
|
861
|
-
};
|
|
862
|
-
function bindItemList(body, owner, key, prefix, kind) {
|
|
863
|
-
const list = owner[key] || {};
|
|
864
|
-
// 删除(二次确认)
|
|
865
|
-
$$('[data-del-item]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
866
|
-
const name = btn.dataset.delItem;
|
|
867
|
-
if (cfg.confirmDelete) {
|
|
868
|
-
const ok = await confirmDialog('删除' + kind, `确定删除 <b>${esc(name)}</b> 吗?<br>该操作仅修改工作区,点「应用修改」前可重新载入放弃。`, '删除');
|
|
869
|
-
if (!ok) return;
|
|
870
|
-
}
|
|
871
|
-
delete list[name];
|
|
872
|
-
addLog('删除' + kind + ':' + name);
|
|
873
|
-
render(); render(); // 双渲染兜底
|
|
874
|
-
}));
|
|
875
|
-
// 新增:内联完整表单(可填全部字段)
|
|
876
|
-
const addBtn = $('#{prefix}-add'.replace('{prefix}', prefix), body);
|
|
877
|
-
addBtn && addBtn.addEventListener('click', () => {
|
|
878
|
-
const nameEl = $('#{prefix}-newname'.replace('{prefix}', prefix), body);
|
|
879
|
-
const name = (nameEl.value || '').trim();
|
|
880
|
-
if (!name) { toast('请输入名称', 'warning'); return; }
|
|
881
|
-
if (!isSafeKey(name)) { toast('该名称不可用', 'warning'); return; }
|
|
882
|
-
if (list[name]) { toast('已存在同名' + kind, 'warning'); return; }
|
|
883
|
-
const addRow = nameEl.closest('.md-row');
|
|
884
|
-
const fields = ITEM_FIELDS[key];
|
|
885
|
-
const ed = inlineEditor(addRow || addBtn.parentElement, fields.map(f => {
|
|
886
|
-
let defVal = '';
|
|
887
|
-
if (f === '品质') defVal = '普通';
|
|
888
|
-
if (f === '数量') defVal = '1';
|
|
889
|
-
return row(f, `<textarea id="ni-${f}" data-nfield="${esc(f)}" data-json="${f === '效果' ? 1 : 0}">${esc(defVal)}</textarea>`, 60);
|
|
890
|
-
}).join('') + `<div style="display:flex;gap:6px;justify-content:flex-end">
|
|
891
|
-
<button class="md-btn mini" id="ni-cancel">取消</button>
|
|
892
|
-
<button class="md-btn mini primary" id="ni-save">${ICONS.check} 确定</button></div>`);
|
|
893
|
-
if (!ed) return;
|
|
894
|
-
$('#ni-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
895
|
-
$('#ni-save', ed).addEventListener('click', () => {
|
|
896
|
-
const tpl = {};
|
|
897
|
-
let okFields = true;
|
|
898
|
-
$$('textarea[data-nfield]', ed).forEach(ta => {
|
|
899
|
-
const field = ta.dataset.nfield;
|
|
900
|
-
let v = ta.value;
|
|
901
|
-
if (field === '效果') {
|
|
902
|
-
const obj = {};
|
|
903
|
-
v.split('\n').forEach(line => { const idx = line.indexOf(':'); if (idx > 0) { var kk = line.slice(0, idx).trim(); if (isSafeKey(kk)) obj[kk] = line.slice(idx + 1).trim(); } });
|
|
904
|
-
tpl[field] = obj;
|
|
905
|
-
} else if (field === '标签') tpl[field] = v.split(/[,,]/).map(s => s.trim()).filter(Boolean);
|
|
906
|
-
else if (field === '数量') tpl[field] = num(v);
|
|
907
|
-
else tpl[field] = v;
|
|
908
|
-
});
|
|
909
|
-
if (!tpl['类型']) { toast('请填写类型', 'error'); okFields = false; }
|
|
910
|
-
if (!okFields) return;
|
|
911
|
-
list[name] = tpl;
|
|
912
|
-
addLog('添加' + kind + ':' + name);
|
|
913
|
-
render(); render();
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
// 点击条目行 → 行下方内联展开编辑
|
|
917
|
-
$$('[data-item]', body).forEach(itemRow => itemRow.addEventListener('click', (e) => {
|
|
918
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
919
|
-
const name = itemRow.getAttribute('data-item');
|
|
920
|
-
const item = list[name];
|
|
921
|
-
if (!item) return;
|
|
922
|
-
let f = '';
|
|
923
|
-
for (const field of ITEM_FIELDS[key]) {
|
|
924
|
-
let val = item[field];
|
|
925
|
-
if (field === '标签') val = (val || []).join(',');
|
|
926
|
-
if (field === '效果') val = Object.entries(val || {}).map(([k, v]) => k + ': ' + v).join('\n');
|
|
927
|
-
f += row(field, `<textarea id="if-${field}" data-json="${field === '效果' ? 1 : 0}">${esc(val ?? '')}</textarea>`, 60);
|
|
928
|
-
}
|
|
929
|
-
const ed = inlineEditor(itemRow, f + `<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini primary" id="if-done">${ICONS.check} 完成</button></div>`);
|
|
930
|
-
if (!ed) return;
|
|
931
|
-
$$('textarea[id^="if-"]', ed).forEach(ta => ta.addEventListener('change', () => {
|
|
932
|
-
const field = ta.id.slice(3);
|
|
933
|
-
if (field === '效果') {
|
|
934
|
-
const obj = {};
|
|
935
|
-
ta.value.split('\n').forEach(line => {
|
|
936
|
-
const idx = line.indexOf(':');
|
|
937
|
-
if (idx > 0) { var kk = line.slice(0, idx).trim(); if (isSafeKey(kk)) obj[kk] = line.slice(idx + 1).trim(); }
|
|
938
|
-
});
|
|
939
|
-
item[field] = obj;
|
|
940
|
-
} else if (field === '标签') item[field] = ta.value.split(/[,,]/).map(s => s.trim()).filter(Boolean);
|
|
941
|
-
else item[field] = field === '数量' ? num(ta.value) : ta.value;
|
|
942
|
-
}));
|
|
943
|
-
$('#if-done', ed).addEventListener('click', () => { ed.remove(); });
|
|
944
|
-
}));
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/* v4.3.1 资产列表(asset 字段 + 内部资产,含标签数组/总空间/结算/位置) */
|
|
948
|
-
const ASSET_FIELDS = ['品质', '类型', '标签', '总空间', '结算', '描述', '位置'];
|
|
949
|
-
const ASSET_FIELD_ARR = { '标签': 1 }; // 数组字段
|
|
950
|
-
function renderAssetList(owner, prefix) {
|
|
951
|
-
const list = owner['资产'] || {};
|
|
952
|
-
let items = Object.entries(list).map(([n, v]) => `<div class="md-item" data-asset="${esc(n)}" style="cursor:pointer">
|
|
953
|
-
<span class="md-item-name" data-assetname="${esc(n)}">${esc(n)}</span>
|
|
954
|
-
<span class="md-item-sub">${esc(v['类型'] || v['品质'] || '')} ${esc(v['位置'] || '')}</span>
|
|
955
|
-
<button class="md-btn mini danger" data-del-asset="${esc(n)}">${ICONS.trash}</button>
|
|
956
|
-
</div>`).join('');
|
|
957
|
-
if (!items) items = '<div class="md-empty">暂无资产</div>';
|
|
958
|
-
return `
|
|
959
|
-
<div id="${prefix}-assetlist">${items}</div>
|
|
960
|
-
<div class="md-row" style="margin-top:6px">
|
|
961
|
-
<input type="text" id="${prefix}-assetnewname" placeholder="新资产名称">
|
|
962
|
-
<button class="md-btn mini primary" id="${prefix}-assetadd">${ICONS.plus} 添加</button>
|
|
963
|
-
</div>`;
|
|
964
|
-
}
|
|
965
|
-
function assetParser(ta) {
|
|
966
|
-
const field = ta.dataset.afield;
|
|
967
|
-
let v = ta.value;
|
|
968
|
-
if (ASSET_FIELD_ARR[field]) return v.split(/[,,]/).map(s => s.trim()).filter(Boolean);
|
|
969
|
-
return v;
|
|
970
|
-
}
|
|
971
|
-
function assetValueLabel(item, field) {
|
|
972
|
-
let val = item[field];
|
|
973
|
-
if (ASSET_FIELD_ARR[field]) val = (val || []).join(',');
|
|
974
|
-
return val == null ? '' : val;
|
|
975
|
-
}
|
|
976
|
-
function bindAssetList(body, owner, prefix) {
|
|
977
|
-
const list = owner['资产'] || {};
|
|
978
|
-
// 删除
|
|
979
|
-
$$('[data-del-asset]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
980
|
-
const name = btn.dataset.delAsset;
|
|
981
|
-
if (cfg.confirmDelete) {
|
|
982
|
-
const ok = await confirmDialog('删除资产', `确定删除 <b>${esc(name)}</b> 吗?<br>该操作仅修改工作区,点「应用修改」前可重新载入放弃。`, '删除');
|
|
983
|
-
if (!ok) return;
|
|
984
|
-
}
|
|
985
|
-
delete list[name];
|
|
986
|
-
addLog('删除资产:' + name);
|
|
987
|
-
render(); render();
|
|
988
|
-
}));
|
|
989
|
-
// 新增(内联完整表单,含内部资产为 JSON 文本框)
|
|
990
|
-
const addBtn = $('#{prefix}-assetadd'.replace('{prefix}', prefix), body);
|
|
991
|
-
addBtn && addBtn.addEventListener('click', () => {
|
|
992
|
-
const nameEl = $('#{prefix}-assetnewname'.replace('{prefix}', prefix), body);
|
|
993
|
-
const name = (nameEl.value || '').trim();
|
|
994
|
-
if (!name) { toast('请输入资产名称', 'warning'); return; }
|
|
995
|
-
if (!isSafeKey(name)) { toast('该名称不可用', 'warning'); return; }
|
|
996
|
-
if (list[name]) { toast('已存在同名资产', 'warning'); return; }
|
|
997
|
-
const addRow = nameEl.closest('.md-row');
|
|
998
|
-
const ed = inlineEditor(addRow || addBtn.parentElement, ASSET_FIELDS.map(f => {
|
|
999
|
-
let defVal = '';
|
|
1000
|
-
if (f === '品质') defVal = '普通';
|
|
1001
|
-
return row(f, `<textarea id="na-${f}" data-afield="${esc(f)}">${esc(defVal)}</textarea>`, 64);
|
|
1002
|
-
}).join('') + row('内部资产(JSON)', `<textarea id="na-inner" style="min-height:70px" placeholder='{"内部资产名":{"品质":"","标签":[],"数量":1,"效果":{},"描述":"","总占用空间":""}}'>{}'</textarea>`, 64) + `<div style="display:flex;gap:6px;justify-content:flex-end">
|
|
1003
|
-
<button class="md-btn mini" id="na-cancel">取消</button>
|
|
1004
|
-
<button class="md-btn mini primary" id="na-save">${ICONS.check} 确定</button></div>`);
|
|
1005
|
-
if (!ed) return;
|
|
1006
|
-
$('#na-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1007
|
-
$('#na-save', ed).addEventListener('click', () => {
|
|
1008
|
-
const tpl = {};
|
|
1009
|
-
let okFields = true;
|
|
1010
|
-
$$('textarea[data-afield]', ed).forEach(ta => { tpl[ta.dataset.afield] = assetParser(ta); });
|
|
1011
|
-
if (!tpl['类型']) { toast('请填写类型', 'error'); okFields = false; }
|
|
1012
|
-
let inner = {};
|
|
1013
|
-
try {
|
|
1014
|
-
const v = $('#na-inner', ed) ? $('#na-inner', ed).value.trim() : '{}';
|
|
1015
|
-
inner = v ? JSON.parse(v) : {};
|
|
1016
|
-
if (!inner || typeof inner !== 'object' || Array.isArray(inner)) inner = {};
|
|
1017
|
-
} catch (e) { toast('内部资产 JSON 格式错误', 'error'); okFields = false; }
|
|
1018
|
-
if (!okFields) return;
|
|
1019
|
-
tpl['内部资产'] = inner;
|
|
1020
|
-
list[name] = tpl;
|
|
1021
|
-
addLog('添加资产:' + name);
|
|
1022
|
-
render(); render();
|
|
1023
|
-
});
|
|
1024
|
-
});
|
|
1025
|
-
// 点击条目行 → 行下方内联展开编辑(含内部资产 JSON)
|
|
1026
|
-
$$('[data-asset]', body).forEach(rowItem => rowItem.addEventListener('click', (e) => {
|
|
1027
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
1028
|
-
const name = rowItem.getAttribute('data-asset');
|
|
1029
|
-
const item = list[name];
|
|
1030
|
-
if (!item) return;
|
|
1031
|
-
let f = '';
|
|
1032
|
-
for (const field of ASSET_FIELDS) {
|
|
1033
|
-
f += row(field, `<textarea id="af-${field}" data-afield="${esc(field)}">${esc(assetValueLabel(item, field))}</textarea>`, 64);
|
|
1034
|
-
}
|
|
1035
|
-
f += row('内部资产(JSON)', `<textarea id="af-inner" style="min-height:70px">${esc(JSON.stringify(item['内部资产'] || {}))}</textarea>`, 64);
|
|
1036
|
-
const ed = inlineEditor(rowItem, f + `<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini primary" id="af-done">${ICONS.check} 完成</button></div>`);
|
|
1037
|
-
if (!ed) return;
|
|
1038
|
-
$$('textarea[id^="af-"]', ed).forEach(ta => ta.addEventListener('change', () => {
|
|
1039
|
-
const field = ta.id.slice(3);
|
|
1040
|
-
if (field === 'inner') {
|
|
1041
|
-
try { item['内部资产'] = JSON.parse(ta.value || '{}'); } catch (e) { toast('内部资产 JSON 格式错误', 'warning'); }
|
|
1042
|
-
} else {
|
|
1043
|
-
item[field] = assetParser(ta);
|
|
1044
|
-
}
|
|
1045
|
-
}));
|
|
1046
|
-
$('#af-done', ed).addEventListener('click', () => { ed.remove(); });
|
|
1047
|
-
}));
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
/* 状态效果列表 */
|
|
1051
|
-
function renderBuffList(owner, who) {
|
|
1052
|
-
const b = owner['状态效果'] || {};
|
|
1053
|
-
let items = Object.entries(b).map(([n, v]) => `<div class="md-item" data-buff="${esc(n)}" style="cursor:pointer">
|
|
1054
|
-
<span class="md-item-name" data-buffname="${esc(n)}">${esc(n)}</span>
|
|
1055
|
-
<span class="md-item-sub">${esc(v['类型'] || '')} 层${v['层数'] ?? 1} ${esc(v['剩余时间'] || '')}</span>
|
|
1056
|
-
<button class="md-btn mini danger" data-del-buff="${esc(n)}">${ICONS.trash}</button>
|
|
1057
|
-
</div>`).join('');
|
|
1058
|
-
if (!items) items = '<div class="md-empty">暂无状态</div>';
|
|
1059
|
-
return `
|
|
1060
|
-
<div id="${who}-bufflist">${items}</div>
|
|
1061
|
-
<div class="md-row" style="margin-top:6px">
|
|
1062
|
-
<input type="text" id="${who}-buffname" placeholder="效果名称">
|
|
1063
|
-
<button class="md-btn mini primary" id="${who}-buffadd">${ICONS.plus} 添加</button>
|
|
1064
|
-
</div>`;
|
|
1065
|
-
}
|
|
1066
|
-
function bindBuffs(body, owner, who = 'hero') {
|
|
1067
|
-
const b = owner['状态效果'] || {};
|
|
1068
|
-
$$('[data-del-buff]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
1069
|
-
const name = btn.dataset.delBuff;
|
|
1070
|
-
if (cfg.confirmDelete) {
|
|
1071
|
-
const ok = await confirmDialog('删除状态效果', `确定删除状态 <b>${esc(name)}</b> 吗?`, '删除');
|
|
1072
|
-
if (!ok) return;
|
|
1073
|
-
}
|
|
1074
|
-
delete b[name];
|
|
1075
|
-
addLog('删除状态效果:' + name);
|
|
1076
|
-
render(); render();
|
|
1077
|
-
}));
|
|
1078
|
-
// 行点击 → 内联展开编辑(类型/效果/层数/剩余时间/来源)
|
|
1079
|
-
$$('[data-buff]', body).forEach(rowEl => rowEl.addEventListener('click', (e) => {
|
|
1080
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
1081
|
-
const name = rowEl.getAttribute('data-buff');
|
|
1082
|
-
const v = b[name];
|
|
1083
|
-
if (!v) return;
|
|
1084
|
-
const typSel = `<select id="b-typ"><option value="增益" ${v['类型'] === '增益' ? 'selected' : ''}>增益</option><option value="减益" ${v['类型'] === '减益' ? 'selected' : ''}>减益</option><option value="其他" ${(v['类型'] !== '增益' && v['类型'] !== '减益') ? 'selected' : ''}>其他</option></select>`;
|
|
1085
|
-
const ed = inlineEditor(rowEl,
|
|
1086
|
-
row('类型', typSel) +
|
|
1087
|
-
row('效果', `<textarea id="b-eff">${esc(v['效果'] || '')}</textarea>`) +
|
|
1088
|
-
row('层数', `<input type="number" id="b-lv" value="${esc(v['层数'] ?? 1)}">`) +
|
|
1089
|
-
row('剩余时间', `<input type="text" id="b-dur" value="${esc(v['剩余时间'] || '')}">`) +
|
|
1090
|
-
row('来源', `<input type="text" id="b-src" value="${esc(v['来源'] || '')}">`) +
|
|
1091
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end">
|
|
1092
|
-
<button class="md-btn mini" id="b-cancel">取消</button>
|
|
1093
|
-
<button class="md-btn mini primary" id="b-save">${ICONS.check} 保存</button></div>`);
|
|
1094
|
-
if (!ed) return;
|
|
1095
|
-
$('#b-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1096
|
-
$('#b-save', ed).addEventListener('click', () => {
|
|
1097
|
-
v['类型'] = $('#b-typ', ed).value;
|
|
1098
|
-
v['效果'] = $('#b-eff', ed).value;
|
|
1099
|
-
v['层数'] = num($('#b-lv', ed).value);
|
|
1100
|
-
v['剩余时间'] = $('#b-dur', ed).value;
|
|
1101
|
-
v['来源'] = $('#b-src', ed).value;
|
|
1102
|
-
addLog('编辑状态效果:' + name);
|
|
1103
|
-
render(); render();
|
|
1104
|
-
});
|
|
1105
|
-
}));
|
|
1106
|
-
// 添加:内联空表单(可填具体效果)
|
|
1107
|
-
const addBtn = $('#{who}-buffadd'.replace('{who}', who), body);
|
|
1108
|
-
addBtn && addBtn.addEventListener('click', () => {
|
|
1109
|
-
const nameEl = $('#{who}-buffname'.replace('{who}', who), body);
|
|
1110
|
-
const n = (nameEl.value || '').trim();
|
|
1111
|
-
if (!n) { toast('请输入效果名称', 'warning'); return; }
|
|
1112
|
-
if (b[n]) { toast('已存在同名效果', 'warning'); return; }
|
|
1113
|
-
const addRow = nameEl.closest('.md-row');
|
|
1114
|
-
const ed = inlineEditor(addRow || addBtn.parentElement,
|
|
1115
|
-
row('名称', `<input type="text" id="nb-name" value="${esc(n)}">`) +
|
|
1116
|
-
row('类型', `<select id="nb-typ"><option value="增益" selected>增益</option><option value="减益">减益</option><option value="其他">其他</option></select>`) +
|
|
1117
|
-
row('效果', `<textarea id="nb-eff" placeholder="效果的具体描述,如:速度+2、力量+1"></textarea>`) +
|
|
1118
|
-
row('层数', `<input type="number" id="nb-lv" value="1">`) +
|
|
1119
|
-
row('剩余时间', `<input type="text" id="nb-dur" value="1回合">`) +
|
|
1120
|
-
row('来源', `<input type="text" id="nb-src" value="修改器">`) +
|
|
1121
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end">
|
|
1122
|
-
<button class="md-btn mini" id="nb-cancel">取消</button>
|
|
1123
|
-
<button class="md-btn mini primary" id="nb-save">${ICONS.check} 确定</button></div>`);
|
|
1124
|
-
if (!ed) return;
|
|
1125
|
-
$('#nb-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1126
|
-
$('#nb-save', ed).addEventListener('click', () => {
|
|
1127
|
-
const nn = ($('#nb-name', ed).value || '').trim();
|
|
1128
|
-
if (!nn) { toast('请输入效果名称', 'error'); return; }
|
|
1129
|
-
if (!isSafeKey(nn)) { toast('该名称不可用', 'warning'); return; }
|
|
1130
|
-
b[nn] = {
|
|
1131
|
-
'类型': $('#nb-typ', ed).value,
|
|
1132
|
-
'效果': $('#nb-eff', ed).value,
|
|
1133
|
-
'层数': num($('#nb-lv', ed).value),
|
|
1134
|
-
'剩余时间': $('#nb-dur', ed).value,
|
|
1135
|
-
'来源': $('#nb-src', ed).value
|
|
1136
|
-
};
|
|
1137
|
-
addLog('添加状态效果:' + nn);
|
|
1138
|
-
render(); render();
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
/* 登神长阶 */
|
|
1144
|
-
function renderAscension(hero) {
|
|
1145
|
-
const a = hero['登神长阶'] || {};
|
|
1146
|
-
const vals = o => JSON.stringify(o || {}, null, 1);
|
|
1147
|
-
const ASC_TYPES = ['要素', '权能', '法则'];
|
|
1148
|
-
const ascList = (type) => Object.entries(a[type] || {}).map(([nm, v]) => `<div class="md-item" data-asc-item="${type}|${esc(nm)}" style="cursor:pointer">
|
|
1149
|
-
<span class="md-item-name" data-ascname="${type}|${esc(nm)}">${esc(nm)}</span>
|
|
1150
|
-
<span class="md-item-sub">${esc((v['被动效果'] || '') + ' / ' + (v['主动效果'] || ''))}</span>
|
|
1151
|
-
<button class="md-btn mini danger" data-del-asc="${type}|${esc(nm)}">${ICONS.trash}</button>
|
|
1152
|
-
</div>`).join('') || '<div class="md-empty">暂无' + type + '</div>';
|
|
1153
|
-
return `
|
|
1154
|
-
<div class="md-row"><label style="width:88px">是否开启</label>
|
|
1155
|
-
<select id="md-as-open"><option value="true" ${a['是否开启'] ? 'selected' : ''}>是</option><option value="false" ${a['是否开启'] ? '' : 'selected'}>否</option></select></div>
|
|
1156
|
-
${ASC_TYPES.map(type => `<div style="margin-top:10px">
|
|
1157
|
-
<div style="font-weight:600;color:var(--md-tx,#b8c2e0);font-size:12px;margin-bottom:4px">${type}(${Object.keys(a[type] || {}).length})</div>
|
|
1158
|
-
<div id="as-${type}-list">${ascList(type)}</div>
|
|
1159
|
-
<div class="md-row" style="margin-top:4px">
|
|
1160
|
-
<button class="md-btn mini primary" data-add-asc="${type}">${ICONS.plus} 添加${type}</button>
|
|
1161
|
-
<button class="md-btn mini" data-json-asc="${type}">JSON 编辑</button>
|
|
1162
|
-
</div>
|
|
1163
|
-
<textarea id="as-json-${type}" style="display:none;min-height:80px;margin-top:4px;font-family:monospace;font-size:11px">${esc(vals(a[type]))}</textarea>
|
|
1164
|
-
</div>`).join('')}
|
|
1165
|
-
${row('神位', textInput('as_god', a['神位'] || ''), 88)}
|
|
1166
|
-
${row('神国名称', textInput('as_rn', ((a['神国']||{})['名称']) || ''), 88)}
|
|
1167
|
-
${row('神国描述', `<textarea id="md-as_rd">${esc(((a['神国']||{})['描述']) || '')}</textarea>`, 88)}
|
|
1168
|
-
`;
|
|
1169
|
-
}
|
|
1170
|
-
function bindAscension(body, hero) {
|
|
1171
|
-
if (!hero['登神长阶']) hero['登神长阶'] = {};
|
|
1172
|
-
const a = hero['登神长阶'];
|
|
1173
|
-
const ASC_TYPES = ['要素', '权能', '法则'];
|
|
1174
|
-
const tryJSON = (el, cb) => { try { cb(JSON.parse(el.value)); } catch (e) { toast('JSON 格式错误', 'error'); } };
|
|
1175
|
-
$('#md-as-open', body).addEventListener('change', e => a['是否开启'] = e.target.value === 'true');
|
|
1176
|
-
$('#md-as_god', body).addEventListener('change', e => a['神位'] = e.target.value);
|
|
1177
|
-
if (!a['神国']) a['神国'] = { '名称': '', '描述': '' };
|
|
1178
|
-
$('#md-as_rn', body).addEventListener('change', e => a['神国']['名称'] = e.target.value);
|
|
1179
|
-
$('#md-as_rd', body).addEventListener('change', e => a['神国']['描述'] = e.target.value);
|
|
1180
|
-
// JSON 编辑兜底(切换显示 textarea,change 时解析写回)
|
|
1181
|
-
ASC_TYPES.forEach(type => {
|
|
1182
|
-
const btn = $(`[data-json-asc="${type}"]`, body);
|
|
1183
|
-
const ta = $('#as-json-' + type, body);
|
|
1184
|
-
if (btn && ta) {
|
|
1185
|
-
btn.addEventListener('click', () => { ta.style.display = ta.style.display === 'none' ? 'block' : 'none'; });
|
|
1186
|
-
ta.addEventListener('change', () => tryJSON(ta, v => { a[type] = v; toast(type + '已更新', 'success'); render(); render(); }));
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
// 添加条目:内联表单(名称 + 被动/主动/描述)
|
|
1190
|
-
$$('[data-add-asc]', body).forEach(btn => btn.addEventListener('click', () => {
|
|
1191
|
-
const type = btn.getAttribute('data-add-asc');
|
|
1192
|
-
const ed = inlineEditor(btn.parentElement,
|
|
1193
|
-
row('名称', `<input type="text" id="na-name" placeholder="${type}名称">`) +
|
|
1194
|
-
row('被动效果', `<textarea id="na-pa" placeholder="如:火焰亲和"></textarea>`) +
|
|
1195
|
-
row('主动效果', `<textarea id="na-aa" placeholder="如:燃尽一切"></textarea>`) +
|
|
1196
|
-
row('描述', `<textarea id="na-ds" placeholder="说明文字"></textarea>`) +
|
|
1197
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini" id="na-cancel">取消</button><button class="md-btn mini primary" id="na-save">${ICONS.check} 确定</button></div>`);
|
|
1198
|
-
if (!ed) return;
|
|
1199
|
-
$('#na-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1200
|
-
$('#na-save', ed).addEventListener('click', () => {
|
|
1201
|
-
const nm = ($('#na-name', ed).value || '').trim();
|
|
1202
|
-
if (!nm) { toast('请输入名称', 'error'); return; }
|
|
1203
|
-
if (!isSafeKey(nm)) { toast('该名称不可用', 'warning'); return; }
|
|
1204
|
-
if (!a[type]) a[type] = {};
|
|
1205
|
-
if (a[type][nm]) { toast('已存在同名' + type, 'warning'); return; }
|
|
1206
|
-
a[type][nm] = { '被动效果': $('#na-pa', ed).value, '主动效果': $('#na-aa', ed).value, '描述': $('#na-ds', ed).value };
|
|
1207
|
-
addLog('添加登神' + type + ':' + nm);
|
|
1208
|
-
render(); render();
|
|
1209
|
-
});
|
|
1210
|
-
}));
|
|
1211
|
-
// 条目点击 → 内联展开编辑;删除
|
|
1212
|
-
$$('[data-asc-item]', body).forEach(rowEl => rowEl.addEventListener('click', (e) => {
|
|
1213
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
1214
|
-
const s = rowEl.getAttribute('data-asc-item');
|
|
1215
|
-
const type = s.slice(0, s.indexOf('|'));
|
|
1216
|
-
const nm = s.slice(s.indexOf('|') + 1);
|
|
1217
|
-
const v = (a[type] || {})[nm];
|
|
1218
|
-
if (!v) return;
|
|
1219
|
-
const ed = inlineEditor(rowEl,
|
|
1220
|
-
row('名称', `<input type="text" id="ea-name" value="${esc(nm)}">`) +
|
|
1221
|
-
row('被动效果', `<textarea id="ea-pa">${esc(v['被动效果'] || '')}</textarea>`) +
|
|
1222
|
-
row('主动效果', `<textarea id="ea-aa">${esc(v['主动效果'] || '')}</textarea>`) +
|
|
1223
|
-
row('描述', `<textarea id="ea-ds">${esc(v['描述'] || '')}</textarea>`) +
|
|
1224
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini" id="ea-cancel">取消</button><button class="md-btn mini primary" id="ea-save">${ICONS.check} 保存</button></div>`);
|
|
1225
|
-
if (!ed) return;
|
|
1226
|
-
$('#ea-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1227
|
-
$('#ea-save', ed).addEventListener('click', () => {
|
|
1228
|
-
const nn = ($('#ea-name', ed).value || '').trim();
|
|
1229
|
-
if (!nn) { toast('请输入名称', 'error'); return; }
|
|
1230
|
-
if (!isSafeKey(nn)) { toast('该名称不可用', 'warning'); return; }
|
|
1231
|
-
if (nn !== nm && a[type][nn]) { toast('已存在同名' + type + ':' + nn, 'warning'); return; }
|
|
1232
|
-
const nv = { '被动效果': $('#ea-pa', ed).value, '主动效果': $('#ea-aa', ed).value, '描述': $('#ea-ds', ed).value };
|
|
1233
|
-
if (nn !== nm) { delete a[type][nm]; }
|
|
1234
|
-
a[type][nn] = nv;
|
|
1235
|
-
addLog('编辑登神' + type + ':' + nm);
|
|
1236
|
-
render(); render();
|
|
1237
|
-
});
|
|
1238
|
-
}));
|
|
1239
|
-
$$('[data-del-asc]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
1240
|
-
const s = btn.getAttribute('data-del-asc');
|
|
1241
|
-
const type = s.slice(0, s.indexOf('|'));
|
|
1242
|
-
const nm = s.slice(s.indexOf('|') + 1);
|
|
1243
|
-
if (cfg.confirmDelete) {
|
|
1244
|
-
const ok = await confirmDialog('删除' + type, `确定删除 <b>${esc(nm)}</b> 吗?`, '删除');
|
|
1245
|
-
if (!ok) return;
|
|
1246
|
-
}
|
|
1247
|
-
if (a[type]) delete a[type][nm];
|
|
1248
|
-
addLog('删除登神' + type + ':' + nm);
|
|
1249
|
-
render(); render();
|
|
1250
|
-
}));
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
/* ============ Tab2 NPC ============ */
|
|
1254
|
-
let npcSel = null;
|
|
1255
|
-
function renderNpc(body) {
|
|
1256
|
-
const date = current.date || {};
|
|
1257
|
-
const rel = current.stat_data['关系列表'] || {};
|
|
1258
|
-
const names = Object.keys(rel);
|
|
1259
|
-
let html = '';
|
|
1260
|
-
html += `<div class="md-row"><label style="width:88px">选择NPC</label>
|
|
1261
|
-
<select id="md-npc-sel">${names.map(n => `<option value="${esc(n)}" ${npcSel === n ? 'selected' : ''}>${esc(n)}</option>`).join('')}</select>
|
|
1262
|
-
<button class="md-btn mini primary" id="md-npc-new">${ICONS.plus} 新增</button>
|
|
1263
|
-
<button class="md-btn mini danger" id="md-npc-del">${ICONS.trash}</button></div>`;
|
|
1264
|
-
if (!names.length) {
|
|
1265
|
-
body.innerHTML = html + '<div class="md-empty">关系列表为空</div>';
|
|
1266
|
-
const sel0 = $('#md-npc-sel', body);
|
|
1267
|
-
sel0 && sel0.addEventListener('change', () => { npcSel = sel0.value; render(); });
|
|
1268
|
-
// 空列表也需可新增 NPC(终审修复:此前仅绑 select,新增按钮失效)
|
|
1269
|
-
const newBtn0 = $('#md-npc-new', body);
|
|
1270
|
-
newBtn0 && newBtn0.addEventListener('click', () => {
|
|
1271
|
-
const name = prompt('新NPC名称:');
|
|
1272
|
-
if (!name) return;
|
|
1273
|
-
if (!isSafeKey(name)) { toast('该名称不可用', 'warning'); return; }
|
|
1274
|
-
rel[name] = { '在场': true, '种族': '人类', '身份': [], '职业': [], '生命层级': '第一层级/普通', '等级': 1, '属性': { '力量': 5, '敏捷': 5, '体质': 5, '智力': 5, '精神': 5 }, '生命值': newResObj(), '法力值': newResObj(), '体力值': newResObj(), '状态效果': {}, '背包': {}, '装备': {}, '技能': {}, '资产': {}, '登神长阶': { '是否开启': false, '要素': {}, '权能': {}, '法则': {}, '神位': '', '神国': { '名称': '', '描述': '' } }, '命定契约': false, '好感度': 0, '心里话': '', '背景故事': '' };
|
|
1275
|
-
if (date) {
|
|
1276
|
-
if (!date['npcs']) date['npcs'] = {};
|
|
1277
|
-
if (!date['npcs'][name]) date['npcs'][name] = {};
|
|
1278
|
-
if (date['npcs'][name]['level'] === undefined) date['npcs'][name]['level'] = 1;
|
|
1279
|
-
if (date['npcs'][name]['exp'] === undefined) date['npcs'][name]['exp'] = 0;
|
|
1280
|
-
}
|
|
1281
|
-
npcSel = name;
|
|
1282
|
-
addLog('新增NPC:' + name);
|
|
1283
|
-
render(); render();
|
|
1284
|
-
});
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
const npc = rel[npcSel] || rel[names[0]];
|
|
1288
|
-
if (!npcSel) npcSel = names[0];
|
|
1289
|
-
html += section('基础信息', 'user', `
|
|
1290
|
-
${row('种族', textInput('n_race', npc['种族'] || ''))}
|
|
1291
|
-
${row('生命层级', textInput('n_life', npc['生命层级'] || ''))}
|
|
1292
|
-
<div class="md-grid">
|
|
1293
|
-
${row('等级', numInput('n_level', npc['等级'] ?? 1))}
|
|
1294
|
-
${row('好感度', numInput('n_aff', npc['好感度'] ?? 0))}
|
|
1295
|
-
${row('经验', numInput('n_exp', (date && date['npcs'] && date['npcs'][npcSel] && date['npcs'][npcSel]['exp']) ?? 0))}
|
|
1296
|
-
</div>
|
|
1297
|
-
${row('身份(逗号分隔)', textInput('n_ids', (npc['身份'] || []).join(',')))}
|
|
1298
|
-
${row('职业(逗号分隔)', textInput('n_occ', (npc['职业'] || []).join(',')))}
|
|
1299
|
-
<div class="md-row"><label style="width:88px">在场</label>
|
|
1300
|
-
<select id="md-n_present"><option value="true" ${npc['在场'] ? 'selected' : ''}>是</option><option value="false" ${npc['在场'] ? '' : 'selected'}>否</option></select></div>
|
|
1301
|
-
<div class="md-row"><label style="width:88px">命定契约</label>
|
|
1302
|
-
<select id="md-n_contract"><option value="true" ${npc['命定契约'] ? 'selected' : ''}>是</option><option value="false" ${npc['命定契约'] ? '' : 'selected'}>否</option></select></div>
|
|
1303
|
-
`);
|
|
1304
|
-
html += section('五维', 'spark', `
|
|
1305
|
-
<div class="md-grid">
|
|
1306
|
-
${row('力量', numInput('n_str', (npc['属性']||{})['力量'] ?? 0))}
|
|
1307
|
-
${row('敏捷', numInput('n_agi', (npc['属性']||{})['敏捷'] ?? 0))}
|
|
1308
|
-
${row('体质', numInput('n_con', (npc['属性']||{})['体质'] ?? 0))}
|
|
1309
|
-
${row('智力', numInput('n_int', (npc['属性']||{})['智力'] ?? 0))}
|
|
1310
|
-
${row('精神', numInput('n_wis', (npc['属性']||{})['精神'] ?? 0))}
|
|
1311
|
-
</div>
|
|
1312
|
-
`, false);
|
|
1313
|
-
html += section('资源', 'coin', renderRes(npc, 'n'), false);
|
|
1314
|
-
html += section('状态效果', 'heart', renderBuffList(npc, 'npc'), false);
|
|
1315
|
-
html += section('背包', 'bag', renderItemList(npc, '背包', 'npc_bag', '物品'), false);
|
|
1316
|
-
html += section('装备', 'shield', renderItemList(npc, '装备', 'npc_eq', '装备'), false);
|
|
1317
|
-
html += section('技能', 'sword', renderItemList(npc, '技能', 'npc_sk', '技能'), false);
|
|
1318
|
-
html += section('资产', 'coin', renderAssetList(npc, 'npc'), false);
|
|
1319
|
-
html += section('扮演资料', 'edit', `
|
|
1320
|
-
${row('性格', `<textarea id="md-n-char">${esc(npc['性格'] || '')}</textarea>`)}
|
|
1321
|
-
${row('喜爱', `<textarea id="md-n-likes">${esc(npc['喜爱'] || '')}</textarea>`)}
|
|
1322
|
-
${row('外貌', `<textarea id="md-n-look">${esc(npc['外貌'] || '')}</textarea>`)}
|
|
1323
|
-
${row('着装', `<textarea id="md-n-dress">${esc(npc['着装'] || '')}</textarea>`)}
|
|
1324
|
-
${row('心里话', `<textarea id="md-n-inner">${esc(npc['心里话'] || '')}</textarea>`)}
|
|
1325
|
-
${row('背景故事', `<textarea id="md-n-bg">${esc(npc['背景故事'] || '')}</textarea>`)}
|
|
1326
|
-
`, false);
|
|
1327
|
-
html += `${applyBarHtml()}`;
|
|
1328
|
-
body.innerHTML = html;
|
|
1329
|
-
|
|
1330
|
-
bindFoldCommon(body);
|
|
1331
|
-
|
|
1332
|
-
const sel = $('#md-npc-sel', body);
|
|
1333
|
-
sel.addEventListener('change', () => {
|
|
1334
|
-
npcSel = sel.value;
|
|
1335
|
-
render();
|
|
1336
|
-
});
|
|
1337
|
-
$('#md-npc-new', body).addEventListener('click', () => {
|
|
1338
|
-
const name = prompt('新NPC名称:');
|
|
1339
|
-
if (!name) return;
|
|
1340
|
-
if (!isSafeKey(name)) { toast('该名称不可用', 'warning'); return; }
|
|
1341
|
-
rel[name] = { '在场': true, '种族': '人类', '身份': [], '职业': [], '生命层级': '第一层级/普通', '等级': 1, '属性': { '力量': 5, '敏捷': 5, '体质': 5, '智力': 5, '精神': 5 }, '生命值': newResObj(), '法力值': newResObj(), '体力值': newResObj(), '状态效果': {}, '背包': {}, '装备': {}, '技能': {}, '资产': {}, '登神长阶': { '是否开启': false, '要素': {}, '权能': {}, '法则': {}, '神位': '', '神国': { '名称': '', '描述': '' } }, '命定契约': false, '好感度': 0, '心里话': '', '背景故事': '' };
|
|
1342
|
-
// 同步初始化 date.npcs(NPC 升级逻辑读取),首次改等级/经验可写
|
|
1343
|
-
if (date) {
|
|
1344
|
-
if (!date['npcs']) date['npcs'] = {};
|
|
1345
|
-
if (!date['npcs'][name]) date['npcs'][name] = {};
|
|
1346
|
-
if (date['npcs'][name]['level'] === undefined) date['npcs'][name]['level'] = 1;
|
|
1347
|
-
if (date['npcs'][name]['exp'] === undefined) date['npcs'][name]['exp'] = 0;
|
|
1348
|
-
}
|
|
1349
|
-
npcSel = name; render();
|
|
1350
|
-
});
|
|
1351
|
-
$('#md-npc-del', body).addEventListener('click', async () => {
|
|
1352
|
-
if (cfg.confirmDelete) {
|
|
1353
|
-
const ok = await confirmDialog('删除NPC', `确定删除 <b>${esc(npcSel)}</b> 吗?<br>该角色的好感度/装备/技能等数据将一并移除(date.npcs 中的记录不受影响)。`, '删除');
|
|
1354
|
-
if (!ok) return;
|
|
1355
|
-
}
|
|
1356
|
-
delete rel[npcSel];
|
|
1357
|
-
addLog('删除NPC:' + npcSel);
|
|
1358
|
-
npcSel = null; render();
|
|
1359
|
-
});
|
|
1360
|
-
bindNpc(body, npc);
|
|
1361
|
-
bindRes(body, npc, 'n'); // v4.3.1 NPC 资源
|
|
1362
|
-
bindBuffs(body, npc, 'npc');
|
|
1363
|
-
bindItemList(body, npc, '背包', 'npc_bag', '物品');
|
|
1364
|
-
bindItemList(body, npc, '装备', 'npc_eq', '装备');
|
|
1365
|
-
bindItemList(body, npc, '技能', 'npc_sk', '技能');
|
|
1366
|
-
bindAssetList(body, npc, 'npc'); // v4.3.1 NPC 资产
|
|
1367
|
-
bindApplyBar(body);
|
|
1368
|
-
}
|
|
1369
|
-
function bindNpc(body, npc) {
|
|
1370
|
-
const date = current.date || {};
|
|
1371
|
-
const bind = (id, fn) => {
|
|
1372
|
-
const el = $('#md-' + id, body);
|
|
1373
|
-
el && el.addEventListener('change', () => fn(el));
|
|
1374
|
-
};
|
|
1375
|
-
bind('n_race', e => npc['种族'] = e.value);
|
|
1376
|
-
bind('n_life', e => npc['生命层级'] = e.value);
|
|
1377
|
-
bind('n_level', e => {
|
|
1378
|
-
npc['等级'] = Math.max(1, Math.min(25, num(e.value)));
|
|
1379
|
-
// 同步 date.npcs(角色卡 NPC 升级逻辑读取的字段)——容错创建,对齐正则版
|
|
1380
|
-
if (date) {
|
|
1381
|
-
if (!date['npcs']) date['npcs'] = {};
|
|
1382
|
-
if (!date['npcs'][npcSel]) date['npcs'][npcSel] = {};
|
|
1383
|
-
date['npcs'][npcSel]['level'] = npc['等级'];
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
bind('n_exp', e => {
|
|
1387
|
-
// NPC 经验写 date.npcs[名]['exp'](date 改动请用「应用修改」直接写入提交)
|
|
1388
|
-
const v = Math.max(0, num(e.value));
|
|
1389
|
-
if (!current.date) current.date = {}; // 终审修复:date 缺失时容错创建,避免输入静默丢弃
|
|
1390
|
-
if (current.date) {
|
|
1391
|
-
if (!current.date['npcs']) current.date['npcs'] = {};
|
|
1392
|
-
if (!current.date['npcs'][npcSel]) current.date['npcs'][npcSel] = {};
|
|
1393
|
-
current.date['npcs'][npcSel]['exp'] = v;
|
|
1394
|
-
}
|
|
1395
|
-
});
|
|
1396
|
-
bind('n_aff', e => npc['好感度'] = Math.max(-100, Math.min(100, num(e.value))));
|
|
1397
|
-
bind('n_ids', e => npc['身份'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean));
|
|
1398
|
-
bind('n_occ', e => npc['职业'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean));
|
|
1399
|
-
bind('n_present', e => npc['在场'] = e.value === 'true');
|
|
1400
|
-
bind('n_contract', e => npc['命定契约'] = e.value === 'true');
|
|
1401
|
-
bind('n_str', e => { if (!npc['属性']) npc['属性'] = {}; npc['属性']['力量'] = num(e.value); });
|
|
1402
|
-
bind('n_agi', e => { if (!npc['属性']) npc['属性'] = {}; npc['属性']['敏捷'] = num(e.value); });
|
|
1403
|
-
bind('n_con', e => { if (!npc['属性']) npc['属性'] = {}; npc['属性']['体质'] = num(e.value); });
|
|
1404
|
-
bind('n_int', e => { if (!npc['属性']) npc['属性'] = {}; npc['属性']['智力'] = num(e.value); });
|
|
1405
|
-
bind('n_wis', e => { if (!npc['属性']) npc['属性'] = {}; npc['属性']['精神'] = num(e.value); });
|
|
1406
|
-
bind('n_char', e => npc['性格'] = e.value);
|
|
1407
|
-
bind('n_likes', e => npc['喜爱'] = e.value);
|
|
1408
|
-
bind('n_look', e => npc['外貌'] = e.value);
|
|
1409
|
-
bind('n_dress', e => npc['着装'] = e.value);
|
|
1410
|
-
bind('n_inner', e => npc['心里话'] = e.value);
|
|
1411
|
-
bind('n_bg', e => npc['背景故事'] = e.value);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
/* ============ Tab3 世界 ============ */
|
|
1415
|
-
function renderWorld(body) {
|
|
1416
|
-
const d = current.stat_data;
|
|
1417
|
-
const W = d['世界'] || {};
|
|
1418
|
-
const ev = d['事件'] || {};
|
|
1419
|
-
const news = d['新闻'] || {};
|
|
1420
|
-
const tasks = d['任务列表'] || {};
|
|
1421
|
-
// 解析时间
|
|
1422
|
-
const t = W['时间'] || '';
|
|
1423
|
-
const tm = t.match(/^(.*?)纪元(\d+)年(\d+)月(\d+)日\s*(星期[一二三四五六日天]?)\s*(\d+):(\d+)$/);
|
|
1424
|
-
const tf = { era: tm ? tm[1] : '复兴', y: tm ? tm[2] : '488', m: tm ? tm[3] : '1', dd: tm ? tm[4] : '1', w: tm ? tm[5] : '星期一', hh: tm ? tm[6] : '00', mm: tm ? tm[7] : '00' };
|
|
1425
|
-
let html = '';
|
|
1426
|
-
html += section('时间与地点', 'calendar', `
|
|
1427
|
-
<div class="md-row"><label style="width:88px">纪元名</label>${textInput('w_era', tf.era)}</div>
|
|
1428
|
-
<div class="md-grid">
|
|
1429
|
-
${row('年', numInput('w_y', tf.y))} ${row('月', numInput('w_m', tf.m))}
|
|
1430
|
-
${row('日', numInput('w_dd', tf.dd))} ${row('星期', textInput('w_w', tf.w))}
|
|
1431
|
-
${row('时', numInput('w_hh', tf.hh))} ${row('分', numInput('w_mm', tf.mm))}
|
|
1432
|
-
</div>
|
|
1433
|
-
${row('地点', textInput('w_loc', W['地点'] || ''))}
|
|
1434
|
-
`);
|
|
1435
|
-
// 新闻
|
|
1436
|
-
const newsSections = { '阿斯塔利亚快报': news['阿斯塔利亚快报'], '酒馆留言板': news['酒馆留言板'], '午后茶会': news['午后茶会'] };
|
|
1437
|
-
let newsHtml = '';
|
|
1438
|
-
for (const [k, sec] of Object.entries(newsSections)) {
|
|
1439
|
-
if (!sec) continue;
|
|
1440
|
-
let f = '';
|
|
1441
|
-
let i = 0;
|
|
1442
|
-
for (const [nk, nv] of Object.entries(sec)) {
|
|
1443
|
-
f += `<div class="md-row" style="align-items:flex-start"><label style="width:70px">${esc(nk)}</label><textarea data-news="${esc(k)}|${esc(nk)}" data-news-i="${i++}">${esc(nv)}</textarea></div>`;
|
|
1444
|
-
}
|
|
1445
|
-
newsHtml += section(k, 'globe', f, false);
|
|
1446
|
-
}
|
|
1447
|
-
html += section('新闻(三栏)', 'globe', newsHtml || '<div class="md-empty">无新闻数据</div>', false);
|
|
1448
|
-
// 任务
|
|
1449
|
-
let taskHtml = Object.entries(tasks).map(([tn, tv]) => `<div class="md-item" data-task="${esc(tn)}" style="cursor:pointer">
|
|
1450
|
-
<span class="md-item-name" data-taskname="${esc(tn)}">${esc(tn)}</span>
|
|
1451
|
-
<span class="md-item-sub">${esc(tv['状态'] || '')}</span>
|
|
1452
|
-
<button class="md-btn mini danger" data-del-task="${esc(tn)}">${ICONS.trash}</button>
|
|
1453
|
-
</div>`).join('') || '<div class="md-empty">暂无任务</div>';
|
|
1454
|
-
taskHtml += `
|
|
1455
|
-
<div class="md-row" style="margin-top:6px"><input type="text" id="w-tasknew" placeholder="新任务名称"><button class="md-btn mini primary" id="w-taskadd">${ICONS.plus} 添加</button></div>`;
|
|
1456
|
-
html += section('任务列表', 'sword', taskHtml, false);
|
|
1457
|
-
// 事件
|
|
1458
|
-
html += section('事件状态', 'spark', `
|
|
1459
|
-
<div class="md-row"><label style="width:88px">开关</label>
|
|
1460
|
-
<select id="md-w_ev_open"><option value="true" ${ev['开关'] ? 'selected' : ''}>开</option><option value="false" ${ev['开关'] ? '' : 'selected'}>关</option></select></div>
|
|
1461
|
-
${row('标题', textInput('w_ev_title', ev['标题'] || ''))}
|
|
1462
|
-
${row('阶段', textInput('w_ev_phase', ev['阶段'] || ''))}
|
|
1463
|
-
${row('信号(逗号)', textInput('w_ev_sig', (ev['信号'] || []).join(',')))}
|
|
1464
|
-
<div class="md-note">v4.3.1:信号仅按 <b><event></b> 指令新增,<b>不可删除既有信号</b>、须与其它变量在同一 JSONPatch 内更新。此处仅修改工作区,应用前请保留已有信号。</div>
|
|
1465
|
-
${row('已完成事件', `<textarea id="w_ev_done">${esc((ev['已完成事件'] || []).join('\n'))}</textarea>`)}
|
|
1466
|
-
`, false);
|
|
1467
|
-
// 事件扩展变量:遍历事件未覆盖键(月历/DLC 等二创变量,只显示存在的)
|
|
1468
|
-
const evCovered = ['是否开启', '开关', '标题', '阶段', '信号', '已完成事件'];
|
|
1469
|
-
const evExtra = Object.keys(ev).filter(k => !evCovered.includes(k));
|
|
1470
|
-
if (evExtra.length) {
|
|
1471
|
-
const evRows = evExtra.map(k => {
|
|
1472
|
-
const v = ev[k];
|
|
1473
|
-
const simple = (v === null || ['string', 'number', 'boolean'].includes(typeof v));
|
|
1474
|
-
const valStr = simple ? String(v ?? '') : JSON.stringify(v, null, 1);
|
|
1475
|
-
return `<div class="md-item" style="align-items:center">
|
|
1476
|
-
<span class="md-item-name" style="width:130px;flex:none">${esc(k)}</span>
|
|
1477
|
-
${simple ? `<input type="text" data-evxkey="${esc(k)}" value="${esc(valStr)}" style="flex:1">` : `<textarea data-evxkey="${esc(k)}" style="flex:1;min-height:48px">${esc(valStr)}</textarea>`}
|
|
1478
|
-
</div>`;
|
|
1479
|
-
}).join('');
|
|
1480
|
-
html += section('事件扩展变量(月历/DLC 等)', 'spark', evRows + '<div class="md-note">仅显示事件中存在的扩展键(二创变量);修改经「应用修改」直接写入提交。</div>', false);
|
|
1481
|
-
}
|
|
1482
|
-
html += `${applyBarHtml()}`;
|
|
1483
|
-
body.innerHTML = html;
|
|
1484
|
-
|
|
1485
|
-
bindFoldCommon(body);
|
|
1486
|
-
|
|
1487
|
-
// 绑定
|
|
1488
|
-
const bind = (id, fn) => { const el = $('#md-' + id, body); el && el.addEventListener('change', () => fn(el)); };
|
|
1489
|
-
$$('[data-evxkey]', body).forEach(i => i.addEventListener('change', () => {
|
|
1490
|
-
const k = i.dataset.evxkey;
|
|
1491
|
-
if (i.tagName === 'TEXTAREA') { try { ev[k] = JSON.parse(i.value); } catch (e) { ev[k] = i.value; } }
|
|
1492
|
-
else {
|
|
1493
|
-
const orig = ev[k];
|
|
1494
|
-
const raw = i.value;
|
|
1495
|
-
if (orig === true || orig === false) ev[k] = raw === 'true';
|
|
1496
|
-
else if (typeof orig === 'number') ev[k] = Number(raw);
|
|
1497
|
-
else ev[k] = raw;
|
|
1498
|
-
}
|
|
1499
|
-
}));
|
|
1500
|
-
const mk = () => `${$('#md-w_era', body).value}纪元${$('#md-w_y', body).value}年${$('#md-w_m', body).value}月${$('#md-w_dd', body).value}日 ${$('#md-w_w', body).value} ${String($('#md-w_hh', body).value).padStart(2, '0')}:${String($('#md-w_mm', body).value).padStart(2, '0')}`;
|
|
1501
|
-
['w_era', 'w_y', 'w_m', 'w_dd', 'w_w', 'w_hh', 'w_mm'].forEach(id => bind(id, () => W['时间'] = mk()));
|
|
1502
|
-
bind('w_loc', e => W['地点'] = e.value);
|
|
1503
|
-
bind('w_ev_open', e => ev['开关'] = e.value === 'true');
|
|
1504
|
-
bind('w_ev_title', e => ev['标题'] = e.value);
|
|
1505
|
-
bind('w_ev_phase', e => ev['阶段'] = e.value);
|
|
1506
|
-
bind('w_ev_sig', e => ev['信号'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean));
|
|
1507
|
-
bind('w_ev_done', e => ev['已完成事件'] = e.value.split('\n').map(s => s.trim()).filter(Boolean));
|
|
1508
|
-
// 新闻绑定
|
|
1509
|
-
$$('textarea[data-news]', body).forEach(ta => ta.addEventListener('change', () => {
|
|
1510
|
-
const [k, nk] = ta.dataset.news.split('|');
|
|
1511
|
-
const sec = news[k];
|
|
1512
|
-
if (sec) sec[nk] = ta.value;
|
|
1513
|
-
}));
|
|
1514
|
-
// 任务:行点击内联展开;添加内联表单
|
|
1515
|
-
$$('[data-del-task]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
1516
|
-
const name = btn.dataset.delTask;
|
|
1517
|
-
if (cfg.confirmDelete) {
|
|
1518
|
-
const ok = await confirmDialog('删除任务', `确定删除任务 <b>${esc(name)}</b> 吗?`, '删除');
|
|
1519
|
-
if (!ok) return;
|
|
1520
|
-
}
|
|
1521
|
-
delete tasks[name];
|
|
1522
|
-
addLog('删除任务:' + name);
|
|
1523
|
-
render(); render();
|
|
1524
|
-
}));
|
|
1525
|
-
$('#w-taskadd', body).addEventListener('click', () => {
|
|
1526
|
-
const n = $('#w-tasknew', body).value.trim();
|
|
1527
|
-
if (!n) { toast('请输入任务名称', 'warning'); return; }
|
|
1528
|
-
if (tasks[n]) { toast('已存在同名任务', 'warning'); return; }
|
|
1529
|
-
const addRow = $('#w-tasknew', body).closest('.md-row');
|
|
1530
|
-
const ed = inlineEditor(addRow, row('名称', `<input type="text" id="nt-name" value="${esc(n)}">`) +
|
|
1531
|
-
row('状态', `<input type="text" id="nt-st" value="进行中">`) +
|
|
1532
|
-
row('关注度', `<input type="text" id="nt-at" value="中">`) +
|
|
1533
|
-
row('进展', `<textarea id="nt-pr"></textarea>`) +
|
|
1534
|
-
row('详情', `<textarea id="nt-dt"></textarea>`) +
|
|
1535
|
-
row('目标', `<textarea id="nt-obj"></textarea>`) +
|
|
1536
|
-
row('奖励', `<textarea id="nt-rw"></textarea>`) +
|
|
1537
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini" id="nt-cancel">取消</button><button class="md-btn mini primary" id="nt-save">${ICONS.check} 确定</button></div>`);
|
|
1538
|
-
if (!ed) return;
|
|
1539
|
-
$('#nt-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1540
|
-
$('#nt-save', ed).addEventListener('click', () => {
|
|
1541
|
-
const nn = ($('#nt-name', ed).value || '').trim();
|
|
1542
|
-
if (!nn) { toast('请输入任务名称', 'error'); return; }
|
|
1543
|
-
if (!isSafeKey(nn)) { toast('该名称不可用', 'warning'); return; }
|
|
1544
|
-
tasks[nn] = { '状态': $('#nt-st', ed).value, '关注度': $('#nt-at', ed).value, '进展': $('#nt-pr', ed).value, '详情': $('#nt-dt', ed).value, '目标': $('#nt-obj', ed).value, '奖励': $('#nt-rw', ed).value };
|
|
1545
|
-
addLog('添加任务:' + nn);
|
|
1546
|
-
render(); render();
|
|
1547
|
-
});
|
|
1548
|
-
});
|
|
1549
|
-
$$('[data-task]', body).forEach(rowEl => rowEl.addEventListener('click', (e) => {
|
|
1550
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
1551
|
-
const n = rowEl.getAttribute('data-task');
|
|
1552
|
-
const t = tasks[n];
|
|
1553
|
-
if (!t) return;
|
|
1554
|
-
const ed = inlineEditor(rowEl, Object.entries({ '状态': t['状态'], '关注度': t['关注度'], '进展': t['进展'], '详情': t['详情'], '目标': t['目标'], '奖励': t['奖励'] }).map(([k, v]) =>
|
|
1555
|
-
row(k, `<textarea id="wt-${esc(k)}">${esc(v)}</textarea>`, 60)
|
|
1556
|
-
).join('') + `<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini primary" id="wt-done">${ICONS.check} 完成</button></div>`);
|
|
1557
|
-
if (!ed) return;
|
|
1558
|
-
$$('textarea[id^="wt-"]', ed).forEach(ta => ta.addEventListener('change', () => { t[ta.id.slice(3)] = ta.value; }));
|
|
1559
|
-
$('#wt-done', ed).addEventListener('click', () => { ed.remove(); });
|
|
1560
|
-
}));
|
|
1561
|
-
bindApplyBar(body);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
/* ============ Tab4 日历/date 变量 ============ */
|
|
1565
|
-
function renderCal(body) {
|
|
1566
|
-
const date = current.date || {};
|
|
1567
|
-
const npcs = date['npcs'] || {};
|
|
1568
|
-
const log = date['log'] || {};
|
|
1569
|
-
const cal = current.calendar || {};
|
|
1570
|
-
let html = '';
|
|
1571
|
-
// NPC 等级经验表
|
|
1572
|
-
let npcRows = Object.entries(npcs).map(([n, v]) => `<div class="md-item">
|
|
1573
|
-
<span class="md-item-name" style="width:110px">${esc(n)}</span>
|
|
1574
|
-
<input type="number" class="cal-level" data-npc="${esc(n)}" value="${esc(v['level'] ?? 1)}" style="width:56px" title="等级">
|
|
1575
|
-
<input type="number" class="cal-exp" data-npc="${esc(n)}" value="${esc(v['exp'] ?? 0)}" style="width:80px" title="经验">
|
|
1576
|
-
<input type="number" class="cal-req" data-npc="${esc(n)}" value="${esc(v['required_exp'] ?? 120)}" style="width:80px" title="升级所需">
|
|
1577
|
-
</div>`).join('') || '<div class="md-empty">date.npcs 为空</div>';
|
|
1578
|
-
html += section('date.npcs 等级/经验(含未在关系列表的NPC)', 'group', npcRows, false);
|
|
1579
|
-
// log
|
|
1580
|
-
html += section('date.log 统计', 'terminal', `
|
|
1581
|
-
<div class="md-grid">
|
|
1582
|
-
${row('死亡次数', numInput('d_death', log['deathCount'] ?? 0))}
|
|
1583
|
-
${row('总FP获取', numInput('d_fp', log['totalFPGained'] ?? 0))}
|
|
1584
|
-
${row('破产次数', numInput('d_bank', log['bankruptcyCount'] ?? 0))}
|
|
1585
|
-
${row('最大负债', numInput('d_debt', log['maxCurrencyDebt'] ?? 0))}
|
|
1586
|
-
</div>
|
|
1587
|
-
`, false);
|
|
1588
|
-
// 配置开关
|
|
1589
|
-
html += section('date 配置', 'gear', `
|
|
1590
|
-
<div class="md-row"><label style="width:150px">NPC随玩家升级</label>
|
|
1591
|
-
<select id="md-d-upw"><option value="true" ${date['npcLevelUpWithPlayer'] ? 'selected' : ''}>是</option><option value="false" ${date['npcLevelUpWithPlayer'] ? '' : 'selected'}>否</option></select></div>
|
|
1592
|
-
<div class="md-row"><label style="width:150px">契约才能得经验</label>
|
|
1593
|
-
<select id="md-d-cont"><option value="true" ${date['requiresContractForExp'] ? 'selected' : ''}>是</option><option value="false" ${date['requiresContractForExp'] ? '' : 'selected'}>否</option></select></div>
|
|
1594
|
-
<div class="md-row"><label style="width:150px">登神法则就绪</label>
|
|
1595
|
-
<select id="md-d-law"><option value="true" ${date['ascensionLawReady'] ? 'selected' : ''}>是</option><option value="false" ${date['ascensionLawReady'] ? '' : 'selected'}>否</option></select></div>
|
|
1596
|
-
`, false);
|
|
1597
|
-
// calendar_float_store
|
|
1598
|
-
html += section('calendar_float_store', 'calendar', `
|
|
1599
|
-
${row('known_tags(逗号)', textInput('cal_tags', (((cal['runtime']||{})['known_tags']) || []).join(',')))}
|
|
1600
|
-
<div class="md-note">此处编辑的是 calendar_float_store.runtime.known_tags,用于日历事件标签白名单。</div>
|
|
1601
|
-
`, false);
|
|
1602
|
-
// 动态扩展变量:遍历 date 未覆盖键(二创变量等,只显示存在的)
|
|
1603
|
-
const calCovered = ['npcs', 'log', 'npcLevelUpWithPlayer', 'requiresContractForExp', 'ascensionLawReady', 'known_tags'];
|
|
1604
|
-
const extraKeys = Object.keys(date).filter(k => !calCovered.includes(k));
|
|
1605
|
-
if (extraKeys.length) {
|
|
1606
|
-
const exRows = extraKeys.map(k => {
|
|
1607
|
-
const v = date[k];
|
|
1608
|
-
const simple = (v === null || ['string', 'number', 'boolean'].includes(typeof v));
|
|
1609
|
-
const valStr = simple ? String(v ?? '') : JSON.stringify(v, null, 1);
|
|
1610
|
-
return `<div class="md-item" style="align-items:center">
|
|
1611
|
-
<span class="md-item-name" style="width:150px;flex:none">${esc(k)}</span>
|
|
1612
|
-
${simple ? `<input type="text" data-xkey="${esc(k)}" value="${esc(valStr)}" style="flex:1">` : `<textarea data-xkey="${esc(k)}" style="flex:1;min-height:48px">${esc(valStr)}</textarea>`}
|
|
1613
|
-
</div>`;
|
|
1614
|
-
}).join('');
|
|
1615
|
-
html += section('扩展变量(date 其他/二创字段)', 'palette', exRows + '<div class="md-note">仅显示当前快照中存在的 date 键(对象/数组用 JSON 编辑,简单值直接编辑);修改经「应用修改」直接写入提交。</div>', false);
|
|
1616
|
-
}
|
|
1617
|
-
html += `${applyBarHtml()}`;
|
|
1618
|
-
body.innerHTML = html;
|
|
1619
|
-
|
|
1620
|
-
bindFoldCommon(body);
|
|
1621
|
-
|
|
1622
|
-
$$('.cal-level', body).forEach(i => i.addEventListener('change', e => { npcs[e.target.dataset.npc]['level'] = num(e.target.value); }));
|
|
1623
|
-
$$('.cal-exp', body).forEach(i => i.addEventListener('change', e => { npcs[e.target.dataset.npc]['exp'] = num(e.target.value); }));
|
|
1624
|
-
$$('.cal-req', body).forEach(i => i.addEventListener('change', e => { npcs[e.target.dataset.npc]['required_exp'] = num(e.target.value); }));
|
|
1625
|
-
$$('[data-xkey]', body).forEach(i => i.addEventListener('change', () => {
|
|
1626
|
-
const k = i.dataset.xkey;
|
|
1627
|
-
if (i.tagName === 'TEXTAREA') { try { date[k] = JSON.parse(i.value); } catch (e) { date[k] = i.value; } }
|
|
1628
|
-
else {
|
|
1629
|
-
const orig = date[k];
|
|
1630
|
-
const raw = i.value;
|
|
1631
|
-
if (orig === true || orig === false) date[k] = raw === 'true';
|
|
1632
|
-
else if (typeof orig === 'number') date[k] = Number(raw);
|
|
1633
|
-
else date[k] = raw;
|
|
1634
|
-
}
|
|
1635
|
-
}));
|
|
1636
|
-
const bind = (id, fn) => { const el = $('#md-' + id, body); el && el.addEventListener('change', () => fn(el)); };
|
|
1637
|
-
bind('d_death', e => log['deathCount'] = num(e.value));
|
|
1638
|
-
bind('d_fp', e => log['totalFPGained'] = num(e.value));
|
|
1639
|
-
bind('d_bank', e => log['bankruptcyCount'] = num(e.value));
|
|
1640
|
-
bind('d_debt', e => log['maxCurrencyDebt'] = num(e.value));
|
|
1641
|
-
bind('d_upw', e => date['npcLevelUpWithPlayer'] = e.value === 'true');
|
|
1642
|
-
bind('d_cont', e => date['requiresContractForExp'] = e.value === 'true');
|
|
1643
|
-
bind('d_law', e => date['ascensionLawReady'] = e.value === 'true');
|
|
1644
|
-
bind('cal_tags', e => { if (!cal['runtime']) cal['runtime'] = {}; cal['runtime']['known_tags'] = e.value.split(/[,,]/).map(s => s.trim()).filter(Boolean); });
|
|
1645
|
-
bindApplyBar(body);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
/* ============ Tab 开局(检测自定义开局 creator-app + 修改转生点) ============ */
|
|
1649
|
-
// 从 Vue app / Pinia 找到自定义开局的 character store(reincarnationPoints 所在)
|
|
1650
|
-
// creator-app 由角色卡「自定义开局」正则注入消息渲染 iframe 的 #app;此处尽力探测。
|
|
1651
|
-
// 增强:穿透当前 window / parent / top / 全部子 iframe,并双重定位(#app 的 __vue_app__,否则全文档扫 __vue_app__)。
|
|
1652
|
-
function _docOf(w) { try { return (w && w.document) || P.document || document; } catch (e) { return null; } }
|
|
1653
|
-
function _storeFromApp(app) {
|
|
1654
|
-
if (!app) return null;
|
|
1655
|
-
const provides = (app._context && app._context.provides) || {};
|
|
1656
|
-
let pinia = null;
|
|
1657
|
-
// Pinia 常以 Symbol key 注入 provides,for...in 会漏掉 → 用 Reflect.ownKeys 枚举 symbol+string key
|
|
1658
|
-
let keys;
|
|
1659
|
-
try { keys = Reflect.ownKeys(provides); } catch (e) { keys = null; }
|
|
1660
|
-
const tryVal = (v) => { if (v && typeof v === 'object' && v._s && !Array.isArray(v)) { pinia = v; return true; } return false; };
|
|
1661
|
-
if (keys) { for (let i = 0; i < keys.length; i++) { if (tryVal(provides[keys[i]])) break; } }
|
|
1662
|
-
if (!pinia) { for (const k in provides) { if (tryVal(provides[k])) break; } }
|
|
1663
|
-
if (!pinia) return null;
|
|
1664
|
-
const store = pinia._s && pinia._s.get ? pinia._s.get('character') : (pinia._s && pinia._s['character']);
|
|
1665
|
-
// 特征校验:只有真正是 creator-app 的 useCharacter store(含 character 或 updateCharacterField)才接受,
|
|
1666
|
-
// 防止命中聊天里其它同名/相似 Vue 应用(如 ST 自身或别的脚本的 #app)。
|
|
1667
|
-
if (!store) return null;
|
|
1668
|
-
const looksLikeCreator = (typeof store.character !== 'undefined' || typeof store.updateCharacterField === 'function');
|
|
1669
|
-
return looksLikeCreator ? store : null;
|
|
1670
|
-
}
|
|
1671
|
-
function _findInDoc(doc) {
|
|
1672
|
-
if (!doc) return null;
|
|
1673
|
-
try {
|
|
1674
|
-
const appEl = doc.querySelector ? doc.querySelector('#app') : null;
|
|
1675
|
-
if (appEl && appEl.__vue_app__) { const s = _storeFromApp(appEl.__vue_app__); if (s) return s; }
|
|
1676
|
-
// 兜底:全文档扫 __vue_app__(挂载点 id 可能非 #app)
|
|
1677
|
-
if (doc.querySelectorAll) {
|
|
1678
|
-
const els = doc.querySelectorAll('[data-v-app],.creator-app,#app');
|
|
1679
|
-
for (let i = 0; i < els.length; i++) { const s = _storeFromApp(els[i].__vue_app__); if (s) return s; }
|
|
1680
|
-
}
|
|
1681
|
-
} catch (e) { return null; }
|
|
1682
|
-
return null;
|
|
1683
|
-
}
|
|
1684
|
-
function findCreatorApp() {
|
|
1685
|
-
const seen = [];
|
|
1686
|
-
const tryW = (w) => {
|
|
1687
|
-
if (!w) return null;
|
|
1688
|
-
try { if (seen.indexOf(w) >= 0) return null; seen.push(w); } catch (e) { }
|
|
1689
|
-
const d = _docOf(w);
|
|
1690
|
-
if (d) { const s = _findInDoc(d); if (s) return s; }
|
|
1691
|
-
return null;
|
|
1692
|
-
};
|
|
1693
|
-
let s = tryW(window) || tryW(P) || tryW(P.parent) || tryW(P.top);
|
|
1694
|
-
if (s) return s;
|
|
1695
|
-
// BFS 遍历所有可访问的子 iframe(同源 srcdoc 可访问),直到找到 creator-app 或没有新的 frame
|
|
1696
|
-
try {
|
|
1697
|
-
const framesOf = (w) => { try { return (w && w.frames) ? Array.from(w.frames) : []; } catch (e) { return []; } };
|
|
1698
|
-
let queue = [P.top, P.parent, window].filter(Boolean);
|
|
1699
|
-
let qi = 0;
|
|
1700
|
-
while (qi < queue.length) {
|
|
1701
|
-
const w = queue[qi++];
|
|
1702
|
-
const subs = framesOf(w);
|
|
1703
|
-
for (let i = 0; i < subs.length; i++) {
|
|
1704
|
-
s = tryW(subs[i]);
|
|
1705
|
-
if (s) return s;
|
|
1706
|
-
queue.push(subs[i]); // 继续深挖该 frame 的子 frame
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
} catch (e) { }
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
// 读取转生点(integer),无则默认 1000;返回 {found, value}
|
|
1713
|
-
// 读取转生点:creator-app 的 useCharacter setup store 把转生点存在 store.character.<字段> 里
|
|
1714
|
-
// (state 是 ref({...reincarnationPoints...}),setup store 返回 {character:e,...})
|
|
1715
|
-
function resGet(store) {
|
|
1716
|
-
if (!store) return undefined;
|
|
1717
|
-
let v;
|
|
1718
|
-
// 主路径:store.character(ref 解包后)→ .reincarnationPoints;再兜底 .value / 直接字段
|
|
1719
|
-
try { v = store.character ? store.character.reincarnationPoints : store.reincarnationPoints; } catch (e) { }
|
|
1720
|
-
if (v === undefined) { try { v = store.character && store.character.value && store.character.value.reincarnationPoints; } catch (e) { } }
|
|
1721
|
-
if (v === undefined) { try { v = store.reincarnationPoints; } catch (e) { } }
|
|
1722
|
-
return v;
|
|
1723
|
-
}
|
|
1724
|
-
// 写入转生点:优先 creator-app 的 updateCharacterField action(触发响应式刷新 UI),回退直接改 store.character
|
|
1725
|
-
function resSet(store, val) {
|
|
1726
|
-
if (!store) return false;
|
|
1727
|
-
const n = typeof val === 'number' ? Math.max(0, Math.floor(val)) : val;
|
|
1728
|
-
try {
|
|
1729
|
-
if (typeof store.updateCharacterField === 'function') { store.updateCharacterField('reincarnationPoints', n); return true; }
|
|
1730
|
-
} catch (e) { }
|
|
1731
|
-
try {
|
|
1732
|
-
if (store.character) store.character.reincarnationPoints = n;
|
|
1733
|
-
else store.reincarnationPoints = n;
|
|
1734
|
-
return true;
|
|
1735
|
-
} catch (e) { return false; }
|
|
1736
|
-
}
|
|
1737
|
-
function readReincarnation() {
|
|
1738
|
-
const store = findCreatorApp();
|
|
1739
|
-
if (!store) return { found: false, value: null };
|
|
1740
|
-
const v = resGet(store);
|
|
1741
|
-
const n = typeof v === 'number' ? v : (typeof v === 'string' ? parseInt(v, 10) : NaN);
|
|
1742
|
-
return { found: true, value: isNaN(n) ? null : n };
|
|
1743
|
-
}
|
|
1744
|
-
// 写入转生点(改 Pinia store 的响应式字段),成功返回 true
|
|
1745
|
-
function writeReincarnation(val) {
|
|
1746
|
-
const store = findCreatorApp();
|
|
1747
|
-
if (!store) return false;
|
|
1748
|
-
return resSet(store, val);
|
|
1749
|
-
}
|
|
1750
|
-
// 是否处于"开局":creator-app 已渲染且其 character store 可用
|
|
1751
|
-
function isOpeningActive() { return !!findCreatorApp(); }
|
|
1752
|
-
let startCache = null; // 开局状态缓存(供快速刷新展示)
|
|
1753
|
-
function renderStart(body) {
|
|
1754
|
-
let html = '';
|
|
1755
|
-
const info = readReincarnation();
|
|
1756
|
-
const opening = info.found;
|
|
1757
|
-
html += section('开局状态检测', 'spark', `
|
|
1758
|
-
<div class="md-note">检测当前是否处于<b>自定义开局</b>流程(角色卡「自定义开局」正则注入的 creator-app 是否已渲染,其 Pinia store 含 <code>reincarnationPoints</code> 转生点)。</div>
|
|
1759
|
-
<div class="md-row"><label style="width:88px">开局状态</label>
|
|
1760
|
-
<span id="md-start-status" style="color:${opening ? 'var(--md-ok,#8fd3a8)' : 'var(--md-danger,#ff8fa8)'};font-weight:600">${opening ? '● 开局中(已检测到自定义开局界面)' : '○ 未检测到自定义开局界面'}</span>
|
|
1761
|
-
</div>
|
|
1762
|
-
<div class="md-note" id="md-start-hint">${opening ? '可修改下方转生点,写入后 creator-app 会即时响应(已消费点数校验自动刷新)。' : '请先通过角色卡「自定义开局」触发开局界面(历史消息含 <code><customized></code> 且界面已渲染),再回来修改转生点。'}</div>
|
|
1763
|
-
`, true);
|
|
1764
|
-
html += section('转生点(reincarnationPoints)', 'coin', `
|
|
1765
|
-
<div class="md-grid">
|
|
1766
|
-
<div class="md-row"><label style="width:88px">当前转生点</label>
|
|
1767
|
-
<input type="number" id="md-reinc" value="${esc(info.found && info.value != null ? info.value : 1000)}" ${opening ? '' : 'disabled'}></div>
|
|
1768
|
-
<div class="md-note" style="grid-column:1/-1">默认 1000;角色名含 <code>[dev]</code>/<code>[test]</code> 时点「随机按名字」可得 666666(creator-app 的自带机制)。转生点用于在自定义开局中消费种族/身份/装备/道具/资产/技能/伙伴/背景等费用。</div>
|
|
1769
|
-
</div>
|
|
1770
|
-
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">
|
|
1771
|
-
<button class="md-btn primary" id="md-reinc-set">${ICONS.check} 写入转生点</button>
|
|
1772
|
-
<button class="md-btn" id="md-reinc-666">${ICONS.wand} 设 666666</button>
|
|
1773
|
-
<button class="md-btn" id="md-reinc-1k">${ICONS.refresh} 重置 1000</button>
|
|
1774
|
-
</div>
|
|
1775
|
-
<div class="md-note" id="md-reinc-msg" style="margin-top:6px"></div>
|
|
1776
|
-
`, true);
|
|
1777
|
-
body.innerHTML = html;
|
|
1778
|
-
bindFoldCommon(body);
|
|
1779
|
-
// 写入转生点(到 creator-app)
|
|
1780
|
-
$('#md-reinc-set', body).addEventListener('click', () => {
|
|
1781
|
-
const v = num($('#md-reinc', body).value);
|
|
1782
|
-
if (writeReincarnation(v)) { $('#md-reinc-msg', body).textContent = '✓ 已写入转生点 ' + v + '(creator-app 即时响应)'; addLog('设置转生点:' + v); }
|
|
1783
|
-
else $('#md-reinc-msg', body).textContent = '✕ 无法访问 creator-app,转生点未写入。请确认开局界面已渲染。';
|
|
1784
|
-
});
|
|
1785
|
-
$('#md-reinc-666', body).addEventListener('click', () => {
|
|
1786
|
-
const v = 666666; $('#md-reinc', body).value = v;
|
|
1787
|
-
if (writeReincarnation(v)) { $('#md-reinc-msg', body).textContent = '✓ 已写入 666666'; addLog('设置转生点:666666'); }
|
|
1788
|
-
else $('#md-reinc-msg', body).textContent = '✕ 无法访问 creator-app,本轮未写入;请在界面渲染后再点「写入转生点」。';
|
|
1789
|
-
});
|
|
1790
|
-
$('#md-reinc-1k', body).addEventListener('click', () => {
|
|
1791
|
-
const v = 1000; $('#md-reinc', body).value = v;
|
|
1792
|
-
if (writeReincarnation(v)) { $('#md-reinc-msg', body).textContent = '✓ 已重置为 1000'; addLog('重置转生点:1000'); }
|
|
1793
|
-
else $('#md-reinc-msg', body).textContent = '✕ 无法访问 creator-app,本轮未写入;请在界面渲染后再点「写入转生点」。';
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
/* ============ Tab5 全局(chat_metadata.variables) ============ */
|
|
1798
|
-
function renderGlobal(body) {
|
|
1799
|
-
const vars = current.chatVars || {};
|
|
1800
|
-
const entries = Object.entries(vars);
|
|
1801
|
-
let html = '';
|
|
1802
|
-
html += `<div class="md-note">编辑聊天级模板变量(setvar / chat_metadata.variables)。修改会立即写入并持久化。</div>`;
|
|
1803
|
-
html += `<div class="md-row" style="margin-top:4px"><input type="text" id="g-key" placeholder="新变量名"><button class="md-btn mini primary" id="g-add">${ICONS.plus} 新增</button></div>`;
|
|
1804
|
-
html += entries.map(([k, v]) => `<div class="md-item" data-gvar="${esc(k)}" style="cursor:pointer">
|
|
1805
|
-
<span class="md-item-name" data-gvarname="${esc(k)}" title="点击展开编辑">${esc(k)}</span>
|
|
1806
|
-
<span class="md-item-sub" style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(typeof v === 'string' ? v.slice(0, 30) : JSON.stringify(v).slice(0, 30))}</span>
|
|
1807
|
-
<button class="md-btn mini danger" data-del-gvar="${esc(k)}">${ICONS.trash}</button>
|
|
1808
|
-
</div>`).join('') || '<div class="md-empty">无模板变量</div>';
|
|
1809
|
-
html += `<div style="padding:6px 0;display:flex;gap:6px;justify-content:flex-end">
|
|
1810
|
-
<button class="md-btn primary" id="md-apply">${ICONS.check} 保存全部</button>
|
|
1811
|
-
</div>`;
|
|
1812
|
-
body.innerHTML = html;
|
|
1813
|
-
|
|
1814
|
-
$('#g-add', body).addEventListener('click', () => {
|
|
1815
|
-
const k = $('#g-key', body).value.trim();
|
|
1816
|
-
if (!k) return;
|
|
1817
|
-
if (!isSafeKey(k)) { toast('该变量名不可用', 'warning'); return; }
|
|
1818
|
-
if (vars[k]) { toast('已存在同名变量 ' + k + '(点击该变量行可直接编辑)', 'warning'); return; }
|
|
1819
|
-
writeChatVar(k, '');
|
|
1820
|
-
current.chatVars = readChatVars();
|
|
1821
|
-
render();
|
|
1822
|
-
});
|
|
1823
|
-
$$('[data-del-gvar]', body).forEach(btn => btn.addEventListener('click', async () => {
|
|
1824
|
-
const k = btn.dataset.delGvar;
|
|
1825
|
-
if (cfg.confirmDelete) {
|
|
1826
|
-
const ok = await confirmDialog('删除全局变量', `确定删除变量 <b>${esc(k)}</b> 吗?`, '删除');
|
|
1827
|
-
if (!ok) return;
|
|
1828
|
-
}
|
|
1829
|
-
removeChatVar(k); current.chatVars = readChatVars(); addLog('删除全局变量:' + k); render();
|
|
1830
|
-
}));
|
|
1831
|
-
$$('[data-gvar]', body).forEach(rowEl => rowEl.addEventListener('click', (e) => {
|
|
1832
|
-
if (e.target && e.target.tagName === 'BUTTON') return;
|
|
1833
|
-
const k = rowEl.getAttribute('data-gvar');
|
|
1834
|
-
const v = vars[k];
|
|
1835
|
-
const val = typeof v === 'string' ? v : JSON.stringify(v, null, 1);
|
|
1836
|
-
const ed = inlineEditor(rowEl,
|
|
1837
|
-
row('键名', `<input type="text" value="${esc(k)}" readonly>`) +
|
|
1838
|
-
row('值', `<textarea id="g-val" style="min-height:80px">${esc(val)}</textarea>`) +
|
|
1839
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini" id="g-cancel">取消</button><button class="md-btn mini primary" id="g-save">${ICONS.check} 保存</button></div>`);
|
|
1840
|
-
if (!ed) return;
|
|
1841
|
-
$('#g-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
1842
|
-
$('#g-save', ed).addEventListener('click', () => {
|
|
1843
|
-
const raw = $('#g-val', ed).value;
|
|
1844
|
-
let val2 = raw;
|
|
1845
|
-
try { val2 = JSON.parse(raw); } catch (e) { /* 保持字符串 */ }
|
|
1846
|
-
writeChatVar(k, val2);
|
|
1847
|
-
current.chatVars = readChatVars();
|
|
1848
|
-
addLog('编辑全局变量:' + k);
|
|
1849
|
-
toast('已保存变量 ' + k, 'success');
|
|
1850
|
-
render();
|
|
1851
|
-
});
|
|
1852
|
-
}));
|
|
1853
|
-
$('#md-apply', body).addEventListener('click', () => {
|
|
1854
|
-
current.chatVars = readChatVars();
|
|
1855
|
-
toast('模板变量已同步(修改为即时生效)', 'success');
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
/* ============ Tab6 操作(正文注入操作 + 操作日志) ============ */
|
|
1860
|
-
function renderOps(body) {
|
|
1861
|
-
let html = '';
|
|
1862
|
-
html += `<div style="display:flex;gap:6px;margin-bottom:8px;align-items:center;flex-wrap:wrap">
|
|
1863
|
-
<span class="md-note" style="flex:1">本页为操作区。写入方式的<b>功能说明与小白的操作方法</b>已移至「设置」Tab → 写入方式。
|
|
1864
|
-
<button class="md-btn mini" id="md-goto-settings">${ICONS.gear} 前往说明</button></span>
|
|
1865
|
-
</div>`;
|
|
1866
|
-
// 待提交修改区(应用前可检查/编辑/删除)
|
|
1867
|
-
const pendItems = pending.length ? pending.map((o, i) => {
|
|
1868
|
-
const pv = typeof o.value === 'object' ? JSON.stringify(o.value) : String(o.value ?? '');
|
|
1869
|
-
const opCN = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' }[o.op] || o.op;
|
|
1870
|
-
return `<div class="md-item" data-pend-row="${i}" style="cursor:pointer;display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--md-line,#333a4d)">
|
|
1871
|
-
<span class="md-item-name" data-edit-pend="${i}" style="flex:1">${esc(opCN + ' ' + pathCN(o.path))}</span>
|
|
1872
|
-
<span style="font-size:11px;color:var(--md-tx2,#6b7391)">${esc((pv || '').slice(0, 30))}</span>
|
|
1873
|
-
<button class="md-btn mini danger" data-del-pend="${i}">${ICONS.trash}</button></div>`;
|
|
1874
|
-
}).join('') : '<div class="md-note">暂无待提交修改。在任意 Tab 修改后,点页面底部「保存当前修改」加入此处,再点「正文注入 / 直接写入」应用。</div>';
|
|
1875
|
-
html += section('待提交修改(' + pending.length + ' 条)', 'edit',
|
|
1876
|
-
pendItems + '<div id="pend-editor" style="display:none;margin-top:6px"></div>' + '<div class="md-note" style="margin-top:6px">点语句行展开编辑路径与值;下方「生成并注入差异指令」(正文注入)或「应用修改」(直接写入)将应用这些待提交修改,应用后自动清空。</div>', true);
|
|
1877
|
-
html += section('正文注入操作(默认·推荐)', 'wand', `
|
|
1878
|
-
<div class="md-row" style="flex-wrap:wrap">
|
|
1879
|
-
<button class="md-btn primary" id="md-body-inject">${ICONS.wand} 生成并注入差异指令</button>
|
|
1880
|
-
<button class="md-btn" id="md-body-reprocess">${ICONS.refresh} 触发 MVU 重新处理变量</button>
|
|
1881
|
-
</div>
|
|
1882
|
-
<div class="md-note" id="md-body-status">尚未注入。注入记录可在「注入记录」Tab 查看。</div>
|
|
1883
|
-
`, true);
|
|
1884
|
-
html += section('操作日志', 'terminal', `<div class="md-log" id="md-logbox">${(getPersistLog().length ? getPersistLog() : log).map(l => esc(l)).join('<br>') || '暂无操作'}</div>`, false);
|
|
1885
|
-
html += `<div style="padding:6px 0;display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap">
|
|
1886
|
-
<button class="md-btn" id="md-reload">${ICONS.refresh} 重新载入变量</button>
|
|
1887
|
-
${cfg.writeMode === 'body' ? `<button class="md-btn" id="md-reprocess">${ICONS.refresh} MVU 重演(立即生效)</button>` : ''}
|
|
1888
|
-
<button class="md-btn primary" id="md-apply">${ICONS.check} ${applyBtnLabel()}</button>
|
|
1889
|
-
</div>`;
|
|
1890
|
-
body.innerHTML = html;
|
|
1891
|
-
|
|
1892
|
-
$('#md-goto-settings', body).addEventListener('click', () => {
|
|
1893
|
-
$$('.md-tab', root).forEach(x => x.classList.toggle('active', x.dataset.tab === 'settings'));
|
|
1894
|
-
render();
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
bindFoldCommon(body);
|
|
1898
|
-
|
|
1899
|
-
$('#md-body-inject', body).addEventListener('click', async () => {
|
|
1900
|
-
if (isGenerating()) {
|
|
1901
|
-
await confirmDialog('AI 正在回复,暂禁修改', '<b>AI 正在生成回复中,请等待本轮回复完成后再注入。</b>', '知道了');
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
const ok = await bodyInjectOnly();
|
|
1905
|
-
if (ok) render(); // 刷新面板:待提交列表清空、状态可见
|
|
1906
|
-
});
|
|
1907
|
-
$('#md-body-reprocess', body).addEventListener('click', async () => {
|
|
1908
|
-
try {
|
|
1909
|
-
const mvu = getMvu();
|
|
1910
|
-
if (mvu && typeof mvu.parseMessage === 'function') {
|
|
1911
|
-
const last = findLastAIMessage();
|
|
1912
|
-
if (last) {
|
|
1913
|
-
const result = await mvu.parseMessage(String(last.mes || ''), {});
|
|
1914
|
-
if (result) { $('#md-body-status', body).textContent = 'MVU 已处理指令,变量已更新(本轮生效)。'; toast('MVU 处理完成', 'success'); }
|
|
1915
|
-
else { $('#md-body-status', body).textContent = 'MVU 未返回结果,请手动点击 MVU 面板的「重新处理变量」。'; toast('未检测到可处理指令,请检查注入内容', 'warning'); }
|
|
1916
|
-
}
|
|
1917
|
-
} else if (mvu && typeof mvu.reloadInitVar === 'function') {
|
|
1918
|
-
await mvu.reloadInitVar();
|
|
1919
|
-
$('#md-body-status', body).textContent = '已调用 MVU 重载。';
|
|
1920
|
-
} else {
|
|
1921
|
-
$('#md-body-status', body).textContent = '未找到 MVU 引擎,请手动点击 MVU 面板的「重新处理变量」按钮。';
|
|
1922
|
-
}
|
|
1923
|
-
addLog('触发 MVU 重新处理变量');
|
|
1924
|
-
setTimeout(() => { loadAll(); render(); }, 800);
|
|
1925
|
-
} catch (e) {
|
|
1926
|
-
$('#md-body-status', body).textContent = '触发失败:' + e.message + '。请手动点击 MVU 面板「重新处理变量」。';
|
|
1927
|
-
toast('触发失败,请手动点击 MVU 面板按钮', 'error');
|
|
1928
|
-
}
|
|
1929
|
-
});
|
|
1930
|
-
$('#md-reload', body).addEventListener('click', () => {
|
|
1931
|
-
loadAll();
|
|
1932
|
-
addLog('重新载入变量');
|
|
1933
|
-
render();
|
|
1934
|
-
toast('已重新载入变量', 'success');
|
|
1935
|
-
});
|
|
1936
|
-
// 待提交修改:点语句行展开编辑;删除按钮移除该条
|
|
1937
|
-
$$('[data-edit-pend]', body).forEach(span => span.addEventListener('click', () => {
|
|
1938
|
-
const i = Number(span.getAttribute('data-edit-pend'));
|
|
1939
|
-
const o = pending[i];
|
|
1940
|
-
if (!o) return;
|
|
1941
|
-
const box = $('#pend-editor', body);
|
|
1942
|
-
if (box) {
|
|
1943
|
-
box.innerHTML = `<div class="md-row"><label>路径</label><input id="pend-path" type="text" value="${esc(o.path)}"></div>
|
|
1944
|
-
<div class="md-row"><label>值</label><input id="pend-val" type="text" value="${esc(typeof o.value === 'object' ? JSON.stringify(o.value) : String(o.value ?? ''))}"></div>
|
|
1945
|
-
<div style="display:flex;gap:6px;justify-content:flex-end;padding-top:4px">
|
|
1946
|
-
<button class="md-btn mini" id="pend-cancel">取消</button>
|
|
1947
|
-
<button class="md-btn mini primary" id="pend-save">✓ 保存</button></div>`;
|
|
1948
|
-
box.style.display = 'block';
|
|
1949
|
-
$('#pend-save', box).addEventListener('click', () => {
|
|
1950
|
-
const np = String($('#pend-path', box).value || '').trim();
|
|
1951
|
-
const nv = String($('#pend-val', box).value || '').trim();
|
|
1952
|
-
if (!np.startsWith('/')) { toast('路径需以 / 开头(如 /主角/金钱)', 'error'); return; }
|
|
1953
|
-
pending[i].path = np;
|
|
1954
|
-
try { pending[i].value = JSON.parse(nv); } catch (e) { pending[i].value = nv; }
|
|
1955
|
-
toast('已更新待提交语句', 'success');
|
|
1956
|
-
render();
|
|
1957
|
-
});
|
|
1958
|
-
$('#pend-cancel', box).addEventListener('click', () => { box.style.display = 'none'; });
|
|
1959
|
-
}
|
|
1960
|
-
}));
|
|
1961
|
-
$$('[data-del-pend]', body).forEach(btn => btn.addEventListener('click', () => {
|
|
1962
|
-
const i = Number(btn.getAttribute('data-del-pend'));
|
|
1963
|
-
if (pending[i]) { pending.splice(i, 1); toast('已移除该待提交语句', 'info'); render(); }
|
|
1964
|
-
}));
|
|
1965
|
-
// 操作 Tab 底部「应用修改」= 直接写入(应用待提交修改 + 工作区全量)
|
|
1966
|
-
const ap2 = $('#md-apply', body);
|
|
1967
|
-
if (ap2) ap2.addEventListener('click', async () => { await applyCurrent(); });
|
|
1968
|
-
// 操作 Tab 底部「MVU 重演」= 引擎重演(应用已注入消息里的变量更新指令)
|
|
1969
|
-
const rp2 = $('#md-reprocess', body);
|
|
1970
|
-
if (rp2) rp2.addEventListener('click', async () => { await mvuReprocess(); });
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
/* ============ 统一应用入口(按全局写入方式分派) ============ */
|
|
1974
|
-
function applyBtnLabel() {
|
|
1975
|
-
const map = { direct: '直接写入', body: '正文注入' };
|
|
1976
|
-
return '应用修改(' + (map[cfg.writeMode] || '直接写入') + ')';
|
|
1977
|
-
}
|
|
1978
|
-
// 生成与快照的差异 JSONPatch 语句数组(仅列出变化字段,单条语句,省 token)
|
|
1979
|
-
function buildDiffOps() {
|
|
1980
|
-
const ops = [];
|
|
1981
|
-
const oldSd = (backup && backup.stat_data) || {};
|
|
1982
|
-
const curSd = current.stat_data || {};
|
|
1983
|
-
const walk = (old, cur, path) => {
|
|
1984
|
-
if (JSON.stringify(old) === JSON.stringify(cur)) return;
|
|
1985
|
-
if (old && cur && typeof old === 'object' && typeof cur === 'object' && !Array.isArray(old) && !Array.isArray(cur)) {
|
|
1986
|
-
for (const k of Object.keys(cur)) walk(old ? old[k] : undefined, cur[k], path + '/' + k);
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
ops.push({ op: 'replace', path: path, value: cur });
|
|
1990
|
-
};
|
|
1991
|
-
for (const k of Object.keys(curSd)) {
|
|
1992
|
-
if (k === '事件') continue;
|
|
1993
|
-
walk(oldSd[k], curSd[k], '/' + k);
|
|
1994
|
-
}
|
|
1995
|
-
return ops;
|
|
1996
|
-
}
|
|
1997
|
-
// 保存当前修改:把自上次保存以来的 stat_data 改动并入待提交队列,并前移 stat 基线
|
|
1998
|
-
// (每次保存只含新改动——删除已注入内容后改 B 再保存,不会把 A 再次提交;
|
|
1999
|
-
// date/calendar 改动不入队,经「操作」Tab 直接写入应用)
|
|
2000
|
-
function savePending() {
|
|
2001
|
-
if (!current.stat_data) { toast('无数据可保存(未找到 stat_data 变量)', 'warning'); return; }
|
|
2002
|
-
const ops = buildDiffOps();
|
|
2003
|
-
const dateChanged = dateBase ? JSON.stringify(current.date || {}) !== JSON.stringify(dateBase) : !!current.date;
|
|
2004
|
-
const calChanged = calBase ? JSON.stringify(current.calendar || {}) !== JSON.stringify(calBase) : !!current.calendar;
|
|
2005
|
-
if (!ops.length) {
|
|
2006
|
-
if (dateChanged || calChanged) toast('stat_data 无新改动;日历/快照类改动请到「操作」Tab 点「应用修改」(直接写入)应用', 'info');
|
|
2007
|
-
else toast('没有新的修改(与上次保存一致)', 'info');
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
|
-
pending = pending.concat(ops);
|
|
2011
|
-
backup = deepClone({ stat_data: current.stat_data, date: (backup && backup.date) || current.date });
|
|
2012
|
-
const dateNote = (dateChanged || calChanged) ? ';注意:NPC 等级/经验等日历类改动请用「应用修改」(直接写入)提交,正文注入不含 date' : '';
|
|
2013
|
-
toast('已保存 ' + ops.length + ' 条待提交修改,请前往「操作」Tab 应用' + dateNote, 'success');
|
|
2014
|
-
render();
|
|
2015
|
-
}
|
|
2016
|
-
// 生成正文注入的最小 UpdateVariable 块(仅差异语句,不包裹 ModVariable)
|
|
2017
|
-
function buildFullPatchText(ops) {
|
|
2018
|
-
const payload = JSON.stringify(ops || buildDiffOps(), null, 2).replace(/<\//g, '\\u003C/');
|
|
2019
|
-
return '<UpdateVariable>\n<Analysis>\n- 修改器指令注入:按 JSONPatch 更新以下变量(replace 覆盖指定路径)。\n</Analysis>\n<JSONPatch>\n' + payload + '\n</JSONPatch>\n</UpdateVariable>';
|
|
2020
|
-
}
|
|
2021
|
-
// 找最后一条 AI(角色)回复消息;找不到则回退最后一条消息
|
|
2022
|
-
function findLastAIMessage() {
|
|
2023
|
-
const chat = getChatArray();
|
|
2024
|
-
if (!Array.isArray(chat) || !chat.length) return null;
|
|
2025
|
-
for (let i = chat.length - 1; i >= 0; i--) {
|
|
2026
|
-
const m = chat[i];
|
|
2027
|
-
if (m && !m.is_user && typeof m.mes === 'string') return m;
|
|
2028
|
-
}
|
|
2029
|
-
return chat[chat.length - 1] || null;
|
|
2030
|
-
}
|
|
2031
|
-
// 正文注入:把「单条差异语句」合并进最后一条 AI 回复已有的 <JSONPatch> 变量更新数组里,
|
|
2032
|
-
// 不追加整份快照(避免整棵树作为上下文发送浪费 token)
|
|
2033
|
-
async function bodyInjectOnly() {
|
|
2034
|
-
const ops = pending.slice();
|
|
2035
|
-
if (!ops.length) { toast('没有待提交的修改:请先修改内容,再点「保存当前修改」', 'warning'); return false; }
|
|
2036
|
-
// 注入前增量检测:保存后又改了 stat 但未再保存的改动,提示并入(防静默丢失)
|
|
2037
|
-
let extraOps = [];
|
|
2038
|
-
try { extraOps = buildDiffOps(); } catch (e) { extraOps = []; }
|
|
2039
|
-
const extra = extraOps.filter(o => !ops.some(x => x.path === o.path));
|
|
2040
|
-
let finalOps = ops;
|
|
2041
|
-
let mergedExtra = false;
|
|
2042
|
-
if (extra.length) {
|
|
2043
|
-
const ok = await confirmDialog('检测到未保存的修改', `「保存当前修改」之后你又改了 <b>${extra.length}</b> 项(如 ${esc(extra.slice(0, 2).map(o => pathCN(o.path)).join('、'))}),尚未加入待提交。\n是否一并注入?`, '一并注入', '仅注入已保存');
|
|
2044
|
-
if (ok) { finalOps = ops.concat(extra); mergedExtra = true; }
|
|
2045
|
-
}
|
|
2046
|
-
const last = findLastAIMessage();
|
|
2047
|
-
if (!last || typeof last !== 'object') { toast('未找到可注入的角色回复消息', 'error'); return false; }
|
|
2048
|
-
// 消息一致性比对:AI 已生成新回复(消息与上次读取不一致)则中止,防写错消息(对齐正则版 v1.5.4)
|
|
2049
|
-
const curCheck = readLatestVar('stat_data');
|
|
2050
|
-
if (curCheck && msgBase && JSON.stringify(curCheck ?? null) !== JSON.stringify(msgBase)) {
|
|
2051
|
-
toast('消息已被更新(AI 生成了新回复),已中止注入,请刷新后重试', 'warning');
|
|
2052
|
-
return false;
|
|
2053
|
-
}
|
|
2054
|
-
let mes = String(last.mes || '');
|
|
2055
|
-
// 在已有的 <JSONPatch>...</JSONPatch> 块中合并单条语句;无块则回退整块注入
|
|
2056
|
-
const jpRe = /<JSONPatch>([\s\S]*?)<\/JSONPatch>/i;
|
|
2057
|
-
const jpMatch = mes.match(jpRe);
|
|
2058
|
-
if (jpMatch) {
|
|
2059
|
-
let arr = [];
|
|
2060
|
-
try { arr = JSON.parse(jpMatch[1].trim()); if (!Array.isArray(arr)) arr = []; } catch (e) { arr = []; }
|
|
2061
|
-
// 合并:同 path 覆盖,其余追加
|
|
2062
|
-
for (const op of finalOps) {
|
|
2063
|
-
const idx = arr.findIndex(x => x && x.path === op.path);
|
|
2064
|
-
if (idx >= 0) arr[idx] = op; else arr.push(op);
|
|
2065
|
-
}
|
|
2066
|
-
const newBlock = '<JSONPatch>\n' + JSON.stringify(arr, null, 2).replace(/<\//g, '\\u003C/') + '\n</JSONPatch>';
|
|
2067
|
-
mes = mes.slice(0, jpMatch.index) + newBlock + mes.slice(jpMatch.index + jpMatch[0].length);
|
|
2068
|
-
addLog('正文注入:向最后一条角色回复的 JSONPatch 数组合并 ' + finalOps.length + ' 条语句');
|
|
2069
|
-
toast('已合并 ' + finalOps.length + ' 条变量语句到最后一条角色回复的变量更新栏:点击「MVU 重演」立即生效(本轮可见,不额外占上下文)', 'success');
|
|
2070
|
-
} else {
|
|
2071
|
-
// 该回复没有变量更新栏:追加最小指令块(仅差异语句,仍非全量快照)
|
|
2072
|
-
const block = buildFullPatchText(finalOps);
|
|
2073
|
-
mes = mes.replace(/<ModVariable>[\s\S]*?<\/ModVariable>/g, '').trim();
|
|
2074
|
-
mes += '\n\n' + block;
|
|
2075
|
-
addLog('正文注入:角色回复无 JSONPatch 块,追加最小差异指令块 ' + finalOps.length + ' 条');
|
|
2076
|
-
toast('该回复无变量更新栏,已追加最小差异指令块:点击「MVU 重演」立即生效', 'success');
|
|
2077
|
-
}
|
|
2078
|
-
last.mes = mes;
|
|
2079
|
-
saveChatNow();
|
|
2080
|
-
pushInjLog(finalOps);
|
|
2081
|
-
// 注入成功后清空待提交队列并前移 stat 基线(下次 diff 只含新改动,杜绝重复注入)
|
|
2082
|
-
// 若用户选择了「仅注入已保存」而未并入增量,把增量保留回待提交队列(防静默丢失)
|
|
2083
|
-
if (extra.length && !mergedExtra) pending = extra;
|
|
2084
|
-
else pending = [];
|
|
2085
|
-
backup = deepClone({ stat_data: current.stat_data, date: (backup && backup.date) || current.date });
|
|
2086
|
-
msgBase = current.stat_data ? deepClone(current.stat_data) : null;
|
|
2087
|
-
return true;
|
|
2088
|
-
}
|
|
2089
|
-
// MVU 重演:让引擎解析最近消息中的 UpdateVariable 指令并应用(与角色卡面板「重新处理变量」等效)
|
|
2090
|
-
async function mvuReprocess() {
|
|
2091
|
-
const last = findLastAIMessage();
|
|
2092
|
-
if (!last || typeof last !== 'object') { toast('未找到可处理的消息', 'error'); return false; }
|
|
2093
|
-
try {
|
|
2094
|
-
const mvu = getMvu();
|
|
2095
|
-
if (mvu && typeof mvu.parseMessage === 'function') {
|
|
2096
|
-
const result = await mvu.parseMessage(String(last.mes || ''), {});
|
|
2097
|
-
if (result) {
|
|
2098
|
-
addLog('MVU 重演:引擎已解析并应用变量更新指令');
|
|
2099
|
-
toast('✓ MVU 重演完成,变量已在本轮生效', 'success');
|
|
2100
|
-
setTimeout(() => { loadAll(); render(); }, 800);
|
|
2101
|
-
return true;
|
|
2102
|
-
}
|
|
2103
|
-
toast('MVU 重演执行了,但未返回结果;请确认注入块仍在消息中', 'warning');
|
|
2104
|
-
return false;
|
|
2105
|
-
}
|
|
2106
|
-
if (mvu && typeof mvu.reloadInitVar === 'function') {
|
|
2107
|
-
await mvu.reloadInitVar();
|
|
2108
|
-
addLog('MVU 重演:调用 reloadInitVar');
|
|
2109
|
-
toast('已调用 MVU 引擎重载', 'success');
|
|
2110
|
-
return true;
|
|
2111
|
-
}
|
|
2112
|
-
toast('未找到 MVU 引擎,请手动点击角色卡 MVU 面板的「重新处理变量」按钮', 'warning');
|
|
2113
|
-
return false;
|
|
2114
|
-
} catch (e) {
|
|
2115
|
-
toast('MVU 重演失败:' + e.message + '。请手动点击 MVU 面板「重新处理变量」', 'error');
|
|
2116
|
-
return false;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
// 检测 AI 是否正在生成回复(生成中禁止修改)
|
|
2120
|
-
function isGenerating() {
|
|
2121
|
-
try {
|
|
2122
|
-
const h = hostObject();
|
|
2123
|
-
if (h && h.getContext) {
|
|
2124
|
-
const ctx = h.getContext();
|
|
2125
|
-
if (ctx && ctx.streamingProcessor && ctx.streamingProcessor.isStreaming) return true;
|
|
2126
|
-
if (ctx && ctx.generateInProgress) return true;
|
|
2127
|
-
}
|
|
2128
|
-
const sp = P.streamingProcessor || (P.parent && P.parent.streamingProcessor);
|
|
2129
|
-
if (sp && sp.isStreaming) return true;
|
|
2130
|
-
} catch (e) { /* 忽略 */ }
|
|
2131
|
-
return false;
|
|
2132
|
-
}
|
|
2133
|
-
// 统一应用入口
|
|
2134
|
-
async function applyCurrent(silent) {
|
|
2135
|
-
if (!current.stat_data) { toast('无数据可应用(未找到 stat_data 变量)', 'warning'); return; }
|
|
2136
|
-
if (isGenerating()) {
|
|
2137
|
-
await confirmDialog('AI 正在回复,暂禁修改', '<b>AI 正在生成回复中,此时修改变量会被下一轮生成覆盖或冲突。</b><br><br>请等待本轮回复完成后,再点击「应用修改」。', '知道了');
|
|
2138
|
-
return;
|
|
2139
|
-
}
|
|
2140
|
-
if (cfg.writeMode === 'body') {
|
|
2141
|
-
pushHistory('正文注入');
|
|
2142
|
-
await bodyInjectOnly();
|
|
2143
|
-
render();
|
|
2144
|
-
} else {
|
|
2145
|
-
await applyAll(silent);
|
|
2146
|
-
}
|
|
2147
|
-
render();
|
|
2148
|
-
}
|
|
2149
|
-
// 底部操作栏:保存当前修改(统一入口,应用在「操作」Tab)
|
|
2150
|
-
function applyBarHtml() {
|
|
2151
|
-
let html = `<div style="padding:6px 0;display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap">`;
|
|
2152
|
-
html += `<button class="md-btn primary" id="md-apply">${ICONS.check} 保存当前修改</button></div>`;
|
|
2153
|
-
return html;
|
|
2154
|
-
}
|
|
2155
|
-
function bindApplyBar(body) {
|
|
2156
|
-
const ap = $('#md-apply', body);
|
|
2157
|
-
if (ap) ap.addEventListener('click', () => { savePending(); });
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
/* ============ 应用修改(含二次确认 + 回滚点 + 多写入方式) ============ */
|
|
2161
|
-
// 生成变更摘要(与备份快照对比,供确认弹窗展示)
|
|
2162
|
-
function diffSummary() {
|
|
2163
|
-
const lines = [];
|
|
2164
|
-
const oldSd = (backup && backup.stat_data) || {};
|
|
2165
|
-
const curSd = current.stat_data || {};
|
|
2166
|
-
const walk = (old, cur, path) => {
|
|
2167
|
-
if (JSON.stringify(old) === JSON.stringify(cur)) return;
|
|
2168
|
-
if (old && cur && typeof old === 'object' && typeof cur === 'object' && !Array.isArray(old) && !Array.isArray(cur)) {
|
|
2169
|
-
for (const k of Object.keys(cur)) walk(old ? old[k] : undefined, cur[k], path + '.' + k);
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
lines.push(path.replace(/^\./, ''));
|
|
2173
|
-
};
|
|
2174
|
-
for (const k of Object.keys(curSd)) {
|
|
2175
|
-
if (k === '事件') continue;
|
|
2176
|
-
walk(oldSd[k], curSd[k], k);
|
|
2177
|
-
}
|
|
2178
|
-
return lines.slice(0, 30);
|
|
2179
|
-
}
|
|
2180
|
-
async function applyAll(silent, label) {
|
|
2181
|
-
if (!current.stat_data) { toast('无数据可应用(未找到 stat_data 变量)', 'warning'); return; }
|
|
2182
|
-
// 待提交检查:无待提交、工作区也无相对消息的改动时拒绝(与「保存当前修改」流程闭环)
|
|
2183
|
-
const statChanged = msgBase ? JSON.stringify(current.stat_data) !== JSON.stringify(msgBase) : !!current.stat_data;
|
|
2184
|
-
const dateChanged = dateBase ? JSON.stringify(current.date || {}) !== JSON.stringify(dateBase) : !!current.date;
|
|
2185
|
-
const calChanged = calBase ? JSON.stringify(current.calendar || {}) !== JSON.stringify(calBase) : !!current.calendar;
|
|
2186
|
-
if (!pending.length && !statChanged && !dateChanged && !calChanged) {
|
|
2187
|
-
toast('没有待提交的修改:请先修改内容,再点「保存当前修改」', 'warning');
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
if (isGenerating()) {
|
|
2191
|
-
await confirmDialog('AI 正在回复,暂禁修改', '<b>AI 正在生成回复中,此时直接写入会被下一轮生成覆盖或冲突。</b><br><br>请等待本轮回复完成后,再点击「应用修改」。', '知道了');
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
// 二次确认(默认开启)
|
|
2195
|
-
if (!silent && cfg.confirmApply) {
|
|
2196
|
-
const diff = diffSummary();
|
|
2197
|
-
const bodyTxt = diff.length
|
|
2198
|
-
? '将应用以下变量修改:<ul>' + diff.map(d => `<li>${esc(d)}</li>`).join('') + '</ul>' + (diff.length >= 30 ? '<div class="md-note">(仅显示前 30 项,其余一并应用)</div>' : '')
|
|
2199
|
-
: '未检测到与快照的差异,将按当前工作区内容整体写入。';
|
|
2200
|
-
const ok = await confirmDialog('确认应用修改', bodyTxt, '确认应用');
|
|
2201
|
-
if (!ok) { toast('已取消', 'warning'); return; }
|
|
2202
|
-
}
|
|
2203
|
-
try {
|
|
2204
|
-
// 记录回滚点(应用前)
|
|
2205
|
-
pushHistory(label || '应用修改');
|
|
2206
|
-
let engineOk = false;
|
|
2207
|
-
// ① 引擎优先写入(与状态栏一致:replaceCurrentMvuData 更新引擎缓存并广播事件,界面实时生效)
|
|
2208
|
-
if (cfg.syncMvu) {
|
|
2209
|
-
try {
|
|
2210
|
-
const mvu = getMvu();
|
|
2211
|
-
if (mvu && typeof mvu.replaceCurrentMvuData === 'function') {
|
|
2212
|
-
const data = mvu.getCurrentMvuData ? (mvu.getCurrentMvuData() || {}) : {};
|
|
2213
|
-
if (current.stat_data) data.stat_data = current.stat_data;
|
|
2214
|
-
if (current.date) data.date = current.date;
|
|
2215
|
-
if (current.calendar) data.calendar_float_store = current.calendar;
|
|
2216
|
-
await mvu.replaceCurrentMvuData(data);
|
|
2217
|
-
engineOk = true;
|
|
2218
|
-
}
|
|
2219
|
-
} catch (e) { console.log('[修改器] MVU 引擎写入失败(降级直接写入):', e.message); }
|
|
2220
|
-
}
|
|
2221
|
-
// ② 直接写入 chat 变量(引擎不可用时的兜底,双保险)
|
|
2222
|
-
const ok1 = writeLatestVar('stat_data', current.stat_data);
|
|
2223
|
-
const ok2 = current.date ? writeLatestVar('date', current.date) : true;
|
|
2224
|
-
const ok3 = current.calendar ? writeLatestVar('calendar_float_store', current.calendar) : true;
|
|
2225
|
-
if (!engineOk && !ok1 && !ok2) { toast('写入失败:未找到聊天消息且 MVU 引擎不可用', 'error'); return; }
|
|
2226
|
-
addLog('应用修改:引擎' + (engineOk ? ' ✓' : ' ✗') + ' stat_data' + (ok1 ? ' ✓' : ' ✗') + ' date' + (ok2 ? ' ✓' : ' ✗') + ' calendar' + (ok3 ? ' ✓' : ' ✗'));
|
|
2227
|
-
// 触发聊天保存(持久化到磁盘)
|
|
2228
|
-
if (cfg.autoSave) saveChatNow();
|
|
2229
|
-
// 广播事件让 ST 界面刷新(模拟变量更新完成,状态栏/正文实时可见)
|
|
2230
|
-
try {
|
|
2231
|
-
const es = getEventSource();
|
|
2232
|
-
const et = getEventTypes();
|
|
2233
|
-
const lastMsg = getChatArray()[getChatArray().length - 1];
|
|
2234
|
-
if (es && et) {
|
|
2235
|
-
if (et.MESSAGE_UPDATED && es.emit) es.emit(et.MESSAGE_UPDATED, lastMsg);
|
|
2236
|
-
if (et.MESSAGE_RECEIVED && es.emit) es.emit(et.MESSAGE_RECEIVED, lastMsg);
|
|
2237
|
-
if (et.CHAT_CHANGED && es.emit) es.emit(et.CHAT_CHANGED);
|
|
2238
|
-
if (et.GENERATION_ENDED && es.emit) es.emit(et.GENERATION_ENDED, { isStreaming: false });
|
|
2239
|
-
}
|
|
2240
|
-
} catch (e) { /* 忽略 */ }
|
|
2241
|
-
// ③ 刷新 MVU 状态栏显示:直接写入后调用引擎 getCurrentMvuData 强制重取(部分前端监听 VARIABLE_INITIALIZED)
|
|
2242
|
-
try {
|
|
2243
|
-
const mvu = getMvu();
|
|
2244
|
-
if (mvu && typeof mvu.getCurrentMvuData === 'function') mvu.getCurrentMvuData();
|
|
2245
|
-
} catch (e) { /* 忽略 */ }
|
|
2246
|
-
backup = deepClone({ stat_data: current.stat_data, date: current.date });
|
|
2247
|
-
// 应用成功后清空待提交队列、同步消息基准(下次 diff 只含新改动)
|
|
2248
|
-
pending = [];
|
|
2249
|
-
msgBase = current.stat_data ? deepClone(current.stat_data) : null;
|
|
2250
|
-
dateBase = current.date ? deepClone(current.date) : null;
|
|
2251
|
-
calBase = current.calendar ? deepClone(current.calendar) : null;
|
|
2252
|
-
toast('✓ 修改已生效' + (engineOk ? '(引擎实时写入)' : '(已写入聊天变量)') + (cfg.autoSave ? '并保存' : ''), 'success');
|
|
2253
|
-
render();
|
|
2254
|
-
} catch (e) {
|
|
2255
|
-
toast('应用失败:' + e.message, 'error');
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
/* ============ Tab 备份·历史·注入(合并) ============ */
|
|
2260
|
-
function renderArchive(body) {
|
|
2261
|
-
let html = '';
|
|
2262
|
-
// ① 备份 / 恢复
|
|
2263
|
-
html += section('备份 / 恢复', 'refresh', `
|
|
2264
|
-
<div class="md-note">导出当前工作区的 stat_data 与 date 变量为 JSON 文件(含时间戳命名)。可跨聊天/跨设备保存。</div>
|
|
2265
|
-
<div class="md-row"><button class="md-btn" id="bk-export">${ICONS.refresh} 导出当前快照(下载 JSON)</button></div>
|
|
2266
|
-
<div class="md-note" id="bk-export-status" style="margin-top:4px">最近导出:无</div>
|
|
2267
|
-
<div style="margin-top:8px;border-top:1px dashed var(--md-line,#333a4d);padding-top:8px">
|
|
2268
|
-
<div class="md-note">粘贴之前导出的 JSON 内容到下方输入框,或一键读取剪贴板,点「恢复」。恢复后将覆盖工作区(需再点「应用修改」生效)。</div>
|
|
2269
|
-
<textarea id="bk-input" class="md-json" placeholder='在此粘贴备份 JSON,例如 {"stat_data": {...}, "date": {...}}'></textarea>
|
|
2270
|
-
<div class="md-row" style="margin-top:6px;flex-wrap:wrap">
|
|
2271
|
-
<button class="md-btn" id="bk-paste">${ICONS.edit} 从剪贴板读取</button>
|
|
2272
|
-
<button class="md-btn primary" id="bk-restore">${ICONS.check} 恢复快照到工作区</button>
|
|
2273
|
-
</div>
|
|
2274
|
-
</div>
|
|
2275
|
-
`, true);
|
|
2276
|
-
// ② 修改历史(回滚)
|
|
2277
|
-
html += section('修改历史(回滚)', 'calendar', `
|
|
2278
|
-
<div class="md-note">每次「应用修改 / 正文注入 / 恢复」前自动保存一个回滚点(最多 ${cfg.maxHistory} 条)。点「回滚」恢复该时间点的 stat_data 与 date 并写回聊天;点「详情」查看当时的变量摘要。</div>
|
|
2279
|
-
<div id="hist-list"></div>
|
|
2280
|
-
<div class="md-row" style="margin-top:6px">
|
|
2281
|
-
<button class="md-btn mini danger" id="hist-clear">${ICONS.trash} 清空全部历史</button>
|
|
2282
|
-
</div>
|
|
2283
|
-
`, true);
|
|
2284
|
-
// ③ 正文注入记录
|
|
2285
|
-
html += section('正文注入记录', 'wand', `
|
|
2286
|
-
<div class="md-note">记录每次正文注入的时间、语句数量与具体语句(path + 新值摘要)。</div>
|
|
2287
|
-
<div id="inj-list"></div>
|
|
2288
|
-
<div class="md-row" style="margin-top:6px">
|
|
2289
|
-
<button class="md-btn mini danger" id="inj-clear">${ICONS.trash} 清空注入记录</button>
|
|
2290
|
-
</div>
|
|
2291
|
-
`, true);
|
|
2292
|
-
html += `<div style="padding:6px 0;display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap">
|
|
2293
|
-
<button class="md-btn" id="md-reload">${ICONS.refresh} 重新载入变量</button>
|
|
2294
|
-
</div>`;
|
|
2295
|
-
body.insertAdjacentHTML('beforeend', html);
|
|
2296
|
-
|
|
2297
|
-
bindFoldCommon(body);
|
|
2298
|
-
|
|
2299
|
-
// 备份/恢复绑定
|
|
2300
|
-
$('#bk-export', body).addEventListener('click', () => {
|
|
2301
|
-
const data = JSON.stringify({ stat_data: current.stat_data, date: current.date }, null, 2);
|
|
2302
|
-
const blob = new Blob([data], { type: 'application/json' });
|
|
2303
|
-
const a = P.document.createElement('a');
|
|
2304
|
-
a.href = URL.createObjectURL(blob);
|
|
2305
|
-
a.download = 'mingding_backup_' + new Date().toISOString().slice(0, 10) + '.json';
|
|
2306
|
-
a.click();
|
|
2307
|
-
setTimeout(() => URL.revokeObjectURL(a.href), 3000);
|
|
2308
|
-
$('#bk-export-status', body).textContent = '最近导出:' + new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
2309
|
-
addLog('导出快照备份');
|
|
2310
|
-
toast('快照已导出', 'success');
|
|
2311
|
-
});
|
|
2312
|
-
$('#bk-paste', body).addEventListener('click', async () => {
|
|
2313
|
-
try {
|
|
2314
|
-
if (P.navigator && P.navigator.clipboard) {
|
|
2315
|
-
const txt = await P.navigator.clipboard.readText();
|
|
2316
|
-
if (txt) { $('#bk-input', body).value = txt; toast('已读取剪贴板内容', 'success'); }
|
|
2317
|
-
else toast('剪贴板为空', 'warning');
|
|
2318
|
-
} else toast('浏览器不支持剪贴板读取,请手动粘贴', 'warning');
|
|
2319
|
-
} catch (e) { toast('读取剪贴板失败(浏览器权限):请手动粘贴', 'error'); }
|
|
2320
|
-
});
|
|
2321
|
-
$('#bk-restore', body).addEventListener('click', async () => {
|
|
2322
|
-
const txt = $('#bk-input', body).value.trim();
|
|
2323
|
-
if (!txt) { toast('请先粘贴备份 JSON', 'warning'); return; }
|
|
2324
|
-
let data;
|
|
2325
|
-
try { data = JSON.parse(txt); } catch (e) { toast('JSON 解析失败:' + e.message, 'error'); return; }
|
|
2326
|
-
if (!data.stat_data && !data.date) { toast('备份格式不正确:缺少 stat_data/date', 'error'); return; }
|
|
2327
|
-
if (cfg.confirmApply) {
|
|
2328
|
-
const ok = await confirmDialog('恢复快照', '恢复将<strong>整体覆盖</strong>当前工作区的 stat_data 与 date 变量(需再点应用修改生效)。<br>继续吗?', '继续恢复');
|
|
2329
|
-
if (!ok) return;
|
|
2330
|
-
}
|
|
2331
|
-
if (data.stat_data) current.stat_data = data.stat_data;
|
|
2332
|
-
if (data.date) current.date = data.date;
|
|
2333
|
-
pushHistory('从备份恢复');
|
|
2334
|
-
addLog('从备份恢复快照');
|
|
2335
|
-
toast('已恢复快照到工作区,请点击「应用修改」生效', 'success');
|
|
2336
|
-
render();
|
|
2337
|
-
});
|
|
2338
|
-
$('#md-reload', body).addEventListener('click', () => { loadAll(); addLog('重新载入变量'); render(); toast('已重新载入变量', 'success'); });
|
|
2339
|
-
|
|
2340
|
-
// 修改历史绑定
|
|
2341
|
-
const renderHistList = () => {
|
|
2342
|
-
const box = $('#hist-list', body);
|
|
2343
|
-
if (!box) return;
|
|
2344
|
-
const hist = getHistory();
|
|
2345
|
-
box.innerHTML = hist.length ? hist.map((h, i) => `<div class="md-hist">
|
|
2346
|
-
<span class="md-hist-name">${esc(h.label || '修改')} @ ${esc(h.time)}</span>
|
|
2347
|
-
<button class="md-btn mini" data-detail="${i}">详情</button>
|
|
2348
|
-
<button class="md-btn mini primary" data-roll="${i}">回滚</button>
|
|
2349
|
-
</div>`).join('') : '<div class="md-empty">暂无历史记录。每次应用修改前会自动生成回滚点。</div>';
|
|
2350
|
-
$$('[data-roll]', box).forEach(btn => btn.addEventListener('click', async () => {
|
|
2351
|
-
const h = getHistory()[parseInt(btn.dataset.roll, 10)];
|
|
2352
|
-
if (!h) return;
|
|
2353
|
-
if (cfg.confirmApply) {
|
|
2354
|
-
const ok = await confirmDialog('回滚到该修改点', `将把变量恢复为 <b>${esc(h.label || '修改')}</b>(${esc(h.time)})时的状态,并写回聊天变量。<br>当前未应用的修改会丢失。继续?`, '回滚');
|
|
2355
|
-
if (!ok) return;
|
|
2356
|
-
}
|
|
2357
|
-
if (h.stat_data) current.stat_data = deepClone(h.stat_data);
|
|
2358
|
-
if (h.date) current.date = deepClone(h.date);
|
|
2359
|
-
await applyAll(true, '回滚到「' + (h.label || '修改') + '」');
|
|
2360
|
-
renderArchive(body);
|
|
2361
|
-
}));
|
|
2362
|
-
$$('[data-detail]', box).forEach(btn => btn.addEventListener('click', () => {
|
|
2363
|
-
const h = getHistory()[parseInt(btn.dataset.detail, 10)];
|
|
2364
|
-
if (!h) return;
|
|
2365
|
-
const sd = h.stat_data || {};
|
|
2366
|
-
const summary = [];
|
|
2367
|
-
if (sd['主角']) summary.push('主角 等级' + (sd['主角']['等级'] ?? '?') + ' 金钱' + (sd['主角']['金钱'] ?? '?'));
|
|
2368
|
-
if (sd['世界']) summary.push('世界: ' + String(sd['世界']['时间'] || '').slice(0, 30));
|
|
2369
|
-
if (sd['命运点数'] !== undefined) summary.push('命运点数 ' + sd['命运点数']);
|
|
2370
|
-
const rel = sd['关系列表'] || {};
|
|
2371
|
-
const relNames = Object.keys(rel).slice(0, 10).join('、');
|
|
2372
|
-
if (relNames) summary.push('NPC: ' + relNames);
|
|
2373
|
-
helpDialog('回滚点详情', `时间:${esc(h.time)}<br>标签:${esc(h.label || '修改')}<br><br>` + (summary.length ? summary.map(s => '· ' + esc(s)).join('<br>') : '<div class="md-note">(无摘要数据)</div>'));
|
|
2374
|
-
}));
|
|
2375
|
-
};
|
|
2376
|
-
renderHistList();
|
|
2377
|
-
$('#hist-clear', body).addEventListener('click', async () => {
|
|
2378
|
-
const ok = await confirmDialog('清空修改历史', '将删除全部回滚点,此操作不可恢复。继续?', '清空');
|
|
2379
|
-
if (ok) { clearHistory(); addLog('清空修改历史'); renderHistList(); }
|
|
2380
|
-
});
|
|
2381
|
-
|
|
2382
|
-
// 注入记录绑定
|
|
2383
|
-
const renderInjList = () => {
|
|
2384
|
-
const box = $('#inj-list', body);
|
|
2385
|
-
if (!box) return;
|
|
2386
|
-
const list = getInjLog();
|
|
2387
|
-
box.innerHTML = list.length ? list.map((h, i) => `<div class="md-hist" style="align-items:flex-start">
|
|
2388
|
-
<span class="md-hist-name" style="white-space:normal">${esc(h.time)} · ${h.count} 条语句</span>
|
|
2389
|
-
<button class="md-btn mini" data-inj-detail="${i}">详情</button>
|
|
2390
|
-
</div>`).join('') : '<div class="md-empty">暂无注入记录。</div>';
|
|
2391
|
-
$$('[data-inj-detail]', box).forEach(btn => btn.addEventListener('click', () => {
|
|
2392
|
-
const h = getInjLog()[parseInt(btn.dataset.injDetail, 10)];
|
|
2393
|
-
if (!h) return;
|
|
2394
|
-
const rows = (h.ops || []).map(o => `· <b>${esc(o.op)}</b> <code>${esc(o.path)}</code> → ${esc(o.value)}`).join('<br>');
|
|
2395
|
-
helpDialog('注入详情', `时间:${esc(h.time)}<br>语句数:${h.count}<br><br>${rows || '(无语句)'}`);
|
|
2396
|
-
}));
|
|
2397
|
-
};
|
|
2398
|
-
renderInjList();
|
|
2399
|
-
$('#inj-clear', body).addEventListener('click', async () => {
|
|
2400
|
-
const ok = await confirmDialog('清空注入记录', '将删除全部正文注入记录,此操作不可恢复。继续?', '清空');
|
|
2401
|
-
if (ok) { clearInjLog(); addLog('清空注入记录'); renderInjList(); }
|
|
2402
|
-
});
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
/* ============ Tab 变量解读(解析 AI 回复的变量更新 + 未声明变化对比) ============ */
|
|
2406
|
-
const PATH_CN = {
|
|
2407
|
-
'/世界/时间': '世界·时间', '/世界/地点': '世界·地点',
|
|
2408
|
-
'/任务列表': '任务列表', '/命运点数': '命运点数', '/新闻': '新闻',
|
|
2409
|
-
'/主角/种族': '主角·种族', '/主角/等级': '主角·等级', '/主角/累计经验值': '主角·经验',
|
|
2410
|
-
'/主角/金钱': '主角·金钱', '/主角/属性/力量': '主角·力量', '/主角/属性/敏捷': '主角·敏捷',
|
|
2411
|
-
'/主角/属性/体质': '主角·体质', '/主角/属性/智力': '主角·智力', '/主角/属性/精神': '主角·精神'
|
|
2412
|
-
};
|
|
2413
|
-
function pathCN(p) {
|
|
2414
|
-
if (PATH_CN[p]) return PATH_CN[p];
|
|
2415
|
-
const parts = String(p || '').split('/').filter(Boolean);
|
|
2416
|
-
return parts.length ? parts.slice(-2).join('·') : p;
|
|
2417
|
-
}
|
|
2418
|
-
function parseUpdateBlocks() {
|
|
2419
|
-
const last = findLastAIMessage();
|
|
2420
|
-
if (!last) return null;
|
|
2421
|
-
const mes = String(last.mes || '');
|
|
2422
|
-
const blocks = [];
|
|
2423
|
-
const re = /<UpdateVariable>([\s\S]*?)<\/UpdateVariable>/gi;
|
|
2424
|
-
let m;
|
|
2425
|
-
while ((m = re.exec(mes))) blocks.push(m[1]);
|
|
2426
|
-
if (!blocks.length) return { blocks: [], source: mes.slice(0, 200), last };
|
|
2427
|
-
const out = [];
|
|
2428
|
-
for (const b of blocks) {
|
|
2429
|
-
const analysis = (b.match(/<Analysis>([\s\S]*?)<\/Analysis>/i) || [])[1] || '';
|
|
2430
|
-
const jp = (b.match(/<JSONPatch>([\s\S]*?)<\/JSONPatch>/i) || [])[1] || '';
|
|
2431
|
-
let ops = [];
|
|
2432
|
-
try { ops = JSON.parse(jp); } catch (e) { ops = []; }
|
|
2433
|
-
out.push({ analysis: analysis.trim(), ops });
|
|
2434
|
-
}
|
|
2435
|
-
return { blocks: out, source: '', last };
|
|
2436
|
-
}
|
|
2437
|
-
// 对比最近两条含 stat_data 的消息,找出未在 JSONPatch 中声明的实际变化
|
|
2438
|
-
function findUndeclaredChanges() {
|
|
2439
|
-
try {
|
|
2440
|
-
const chat = getChatArray();
|
|
2441
|
-
const snaps = [];
|
|
2442
|
-
for (let i = chat.length - 1; i >= 0 && snaps.length < 2; i--) {
|
|
2443
|
-
const v = chat[i] && chat[i].variables;
|
|
2444
|
-
if (Array.isArray(v)) {
|
|
2445
|
-
for (const item of v) {
|
|
2446
|
-
if (item && typeof item === 'object' && item.stat_data) { snaps.push({ idx: i, sd: item.stat_data }); break; }
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
if (snaps.length < 2) return [];
|
|
2451
|
-
const [newer, older] = snaps;
|
|
2452
|
-
const declared = new Set();
|
|
2453
|
-
const blocks = parseUpdateBlocks();
|
|
2454
|
-
if (blocks && blocks.blocks) blocks.blocks.forEach(bb => (bb.ops || []).forEach(o => declared.add(o.path)));
|
|
2455
|
-
const diffPaths = [];
|
|
2456
|
-
const walk = (old, cur, path) => {
|
|
2457
|
-
if (JSON.stringify(old) === JSON.stringify(cur)) return;
|
|
2458
|
-
if (old && cur && typeof old === 'object' && typeof cur === 'object' && !Array.isArray(old) && !Array.isArray(cur)) {
|
|
2459
|
-
for (const k of Object.keys(cur)) walk(old ? old[k] : undefined, cur[k], path + '/' + k);
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
diffPaths.push(path);
|
|
2463
|
-
};
|
|
2464
|
-
for (const k of Object.keys(newer.sd)) {
|
|
2465
|
-
if (k === '事件') continue;
|
|
2466
|
-
walk(older.sd[k], newer.sd[k], '/' + k);
|
|
2467
|
-
}
|
|
2468
|
-
return diffPaths.filter(p => !declared.has(p));
|
|
2469
|
-
} catch (e) { return []; }
|
|
2470
|
-
}
|
|
2471
|
-
// 把第 bi 个 <UpdateVariable> 块的 JSONPatch 写回为 ops;ops 为空时移除整个块(对齐正则版 removeUpdBlock)
|
|
2472
|
-
// 变量解读页打开时缓存的消息引用(写回前比对,防 AI 新回复导致写错消息)
|
|
2473
|
-
let updMsgRef = null;
|
|
2474
|
-
function syncPatchToMes(bi, ops) {
|
|
2475
|
-
const last = findLastAIMessage();
|
|
2476
|
-
if (!last || typeof last !== 'object') { toast('未找到可写入的消息', 'error'); return false; }
|
|
2477
|
-
// 一致性防呆:打开变量解读后消息已变化(AI 生成新回复)则中止,提示刷新
|
|
2478
|
-
if (updMsgRef && last !== updMsgRef) { toast('消息已更新(AI 生成了新回复),请先刷新变量解读再编辑', 'warning'); return false; }
|
|
2479
|
-
let mes = String(last.mes || '');
|
|
2480
|
-
const re = /<UpdateVariable>([\s\S]*?)<\/UpdateVariable>/gi;
|
|
2481
|
-
let m, idx = 0, found = null;
|
|
2482
|
-
while ((m = re.exec(mes))) { if (idx === bi) { found = m; break; } idx++; }
|
|
2483
|
-
if (!found) { toast('未找到对应变量更新块(消息可能已变化,请刷新)', 'error'); return false; }
|
|
2484
|
-
const whole = found[0], inner = found[1];
|
|
2485
|
-
if (!ops.length) {
|
|
2486
|
-
mes = mes.slice(0, found.index) + mes.slice(found.index + whole.length);
|
|
2487
|
-
} else {
|
|
2488
|
-
const payload = JSON.stringify(ops, null, 2).replace(/<\//g, '\\u003C/');
|
|
2489
|
-
const newBlock = /<JSONPatch>[\s\S]*?<\/JSONPatch>/i.test(inner)
|
|
2490
|
-
? inner.replace(/<JSONPatch>[\s\S]*?<\/JSONPatch>/i, '<JSONPatch>\n' + payload + '\n</JSONPatch>')
|
|
2491
|
-
: inner.trim() + '\n<JSONPatch>\n' + payload + '\n</JSONPatch>';
|
|
2492
|
-
mes = mes.slice(0, found.index) + '<UpdateVariable>' + newBlock + '</UpdateVariable>' + mes.slice(found.index + whole.length);
|
|
2493
|
-
}
|
|
2494
|
-
last.mes = mes;
|
|
2495
|
-
saveChatNow();
|
|
2496
|
-
return true;
|
|
2497
|
-
}
|
|
2498
|
-
function renderParse(body) {
|
|
2499
|
-
let html = '';
|
|
2500
|
-
html += sectionHelp('本轮变量更新解读', 'edit',
|
|
2501
|
-
`<b>这个页面做什么?</b><br>
|
|
2502
|
-
1. 解析最后一条 AI 回复里「变量更新情况」(<UpdateVariable>) 的内容,把每条 JSONPatch 语句翻译成中文说明。<br>
|
|
2503
|
-
2. 对比最近两条消息的实际变量差异,找出 <b>没有明确写进变量更新、但实际发生了变化</b> 的字段(AI 偷偷改了什么)。`,
|
|
2504
|
-
`<div id="parse-out"><div class="md-empty">正在解析…</div></div>
|
|
2505
|
-
<div class="md-row" style="margin-top:6px">
|
|
2506
|
-
<button class="md-btn" id="parse-refresh">${ICONS.refresh} 重新解析</button>
|
|
2507
|
-
</div>`, true);
|
|
2508
|
-
body.innerHTML = html;
|
|
2509
|
-
$$('.md-help', body).forEach(h => h.addEventListener('click', e => {
|
|
2510
|
-
e.stopPropagation();
|
|
2511
|
-
const box = h.parentElement.nextElementSibling;
|
|
2512
|
-
if (box && box.classList.contains('md-helpbox')) box.classList.toggle('show');
|
|
2513
|
-
}));
|
|
2514
|
-
bindFoldCommon(body);
|
|
2515
|
-
|
|
2516
|
-
const renderParse = () => {
|
|
2517
|
-
const box = $('#parse-out', body);
|
|
2518
|
-
if (!box) return;
|
|
2519
|
-
const res = parseUpdateBlocks();
|
|
2520
|
-
updMsgRef = res ? res.last : null;
|
|
2521
|
-
let out = '';
|
|
2522
|
-
if (!res) { box.innerHTML = '<div class="md-empty">未找到聊天消息</div>'; return; }
|
|
2523
|
-
if (!res.blocks.length) {
|
|
2524
|
-
out = `<div class="md-empty">最后一条回复中没有 <UpdateVariable> 变量更新块。<br><small>可能该回复没有更新变量,或已被正则剥离。</small></div>`;
|
|
2525
|
-
} else {
|
|
2526
|
-
res.blocks.forEach((b, bi) => {
|
|
2527
|
-
out += `<div style="margin-bottom:10px;padding:8px;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#2c3140);border-radius:8px">`;
|
|
2528
|
-
out += `<div style="color:var(--md-accent,#ffd27a);font-weight:600;margin-bottom:4px">变量更新块 #${bi + 1}</div>`;
|
|
2529
|
-
if (b.analysis) out += `<div class="md-note">分析:${esc(b.analysis.slice(0, 300))}</div>`;
|
|
2530
|
-
out += `<div style="margin-top:4px">`;
|
|
2531
|
-
if (b.ops && b.ops.length) {
|
|
2532
|
-
b.ops.forEach((o, oi) => {
|
|
2533
|
-
const v = typeof o.value === 'object' ? JSON.stringify(o.value).slice(0, 80) : String(o.value).slice(0, 80);
|
|
2534
|
-
const opCN = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' }[o.op] || o.op;
|
|
2535
|
-
out += `<div class="md-item" data-parse-row="${bi}|${oi}" style="cursor:pointer;font-size:12px;padding:2px 0;color:var(--md-tx,#cdd6ea)">
|
|
2536
|
-
<span style="flex:1">· <b style="color:${o.op === 'remove' ? 'var(--md-danger,#ff8fa8)' : 'var(--md-ok,#8fd3a8)'}">${opCN}</b> <code style="color:var(--md-tx2,#7d8bb8)">${esc(o.path)}</code> ${o.op === 'remove' ? '' : '→ ' + esc(v)} <span style="color:var(--md-tx2,#6b7391)">(${esc(pathCN(o.path))})</span></span>
|
|
2537
|
-
<button class="md-btn mini" data-edit-parse="${bi}|${oi}">${ICONS.edit} 编辑</button>
|
|
2538
|
-
<button class="md-btn mini danger" data-del-parse="${bi}|${oi}">${ICONS.trash}</button>
|
|
2539
|
-
</div>`;
|
|
2540
|
-
});
|
|
2541
|
-
} else out += '<div class="md-note">JSONPatch 解析为空或格式异常。</div>';
|
|
2542
|
-
out += `</div></div>`;
|
|
2543
|
-
});
|
|
2544
|
-
}
|
|
2545
|
-
// 未声明变化
|
|
2546
|
-
const undeclared = findUndeclaredChanges();
|
|
2547
|
-
out += `<div style="margin-top:8px;padding:8px;background:#1a1410;border:1px solid #7a5b3b;border-radius:8px">
|
|
2548
|
-
<div style="color:#ffb84d;font-weight:600;margin-bottom:4px">⚠ 未明确写入变量更新、但实际发生了变化</div>`;
|
|
2549
|
-
if (undeclared.length) out += undeclared.map(p => `<div style="font-size:12px;padding:2px 0;color:#e0c39a">· <code>${esc(p)}</code> <span style="color:#8a7555">(${esc(pathCN(p))})</span></div>`).join('');
|
|
2550
|
-
else out += `<div class="md-note">未检测到(或最近两条消息间没有 stat_data 快照可对比)。</div>`;
|
|
2551
|
-
out += `</div>`;
|
|
2552
|
-
box.innerHTML = out;
|
|
2553
|
-
// 每次重绘后重新绑定编辑/删除(box 作用域)
|
|
2554
|
-
$$('[data-edit-parse]', box).forEach(btn => btn.addEventListener('click', () => {
|
|
2555
|
-
const parts = btn.getAttribute('data-edit-parse').split('|');
|
|
2556
|
-
const bi = Number(parts[0]), oi = Number(parts[1]);
|
|
2557
|
-
const res2 = parseUpdateBlocks();
|
|
2558
|
-
const bb = res2 && res2.blocks[bi];
|
|
2559
|
-
const o = bb && bb.ops[oi];
|
|
2560
|
-
if (!o) { toast('语句已不存在(消息可能已变化,请刷新)', 'warning'); return; }
|
|
2561
|
-
const rowEl = btn.closest('[data-parse-row]');
|
|
2562
|
-
const opSel = ['replace', 'delta', 'insert', 'remove', 'move'].map(op =>
|
|
2563
|
-
`<option value="${op}" ${o.op === op ? 'selected' : ''}>${{ replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' }[op]}</option>`).join('');
|
|
2564
|
-
const ed = inlineEditor(rowEl,
|
|
2565
|
-
row('操作', `<select id="pe-op">${opSel}</select>`) +
|
|
2566
|
-
row('路径', `<input type="text" id="pe-path" value="${esc(o.path)}">`) +
|
|
2567
|
-
row('值', `<input type="text" id="pe-val" value="${esc(typeof o.value === 'object' ? JSON.stringify(o.value) : String(o.value ?? ''))}">`) +
|
|
2568
|
-
`<div style="display:flex;gap:6px;justify-content:flex-end"><button class="md-btn mini" id="pe-cancel">取消</button><button class="md-btn mini primary" id="pe-save">${ICONS.check} 保存</button></div>`);
|
|
2569
|
-
if (!ed) return;
|
|
2570
|
-
$('#pe-cancel', ed).addEventListener('click', () => { ed.remove(); });
|
|
2571
|
-
$('#pe-save', ed).addEventListener('click', () => {
|
|
2572
|
-
const np = String($('#pe-path', ed).value || '').trim();
|
|
2573
|
-
if (!np.startsWith('/')) { toast('路径需以 / 开头(如 /主角/金钱)', 'error'); return; }
|
|
2574
|
-
o.op = $('#pe-op', ed).value;
|
|
2575
|
-
o.path = np;
|
|
2576
|
-
const raw = $('#pe-val', ed).value;
|
|
2577
|
-
try { o.value = JSON.parse(raw); } catch (e) { o.value = raw; }
|
|
2578
|
-
if (syncPatchToMes(bi, bb.ops)) { addLog('编辑变量更新语句:' + np); toast('已更新并写回消息(可再点 MVU 重演生效)', 'success'); renderParse(); }
|
|
2579
|
-
});
|
|
2580
|
-
}));
|
|
2581
|
-
$$('[data-del-parse]', box).forEach(btn => btn.addEventListener('click', async () => {
|
|
2582
|
-
const parts = btn.getAttribute('data-del-parse').split('|');
|
|
2583
|
-
const bi = Number(parts[0]), oi = Number(parts[1]);
|
|
2584
|
-
const res2 = parseUpdateBlocks();
|
|
2585
|
-
const bb = res2 && res2.blocks[bi];
|
|
2586
|
-
const o = bb && bb.ops[oi];
|
|
2587
|
-
if (!o) return;
|
|
2588
|
-
const opCN = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' }[o.op] || o.op;
|
|
2589
|
-
if (cfg.confirmDelete) {
|
|
2590
|
-
const ok = await confirmDialog('删除变量更新语句', `确定删除语句 <b>${esc(opCN)} ${esc(pathCN(o.path))}</b> 吗?`, '删除');
|
|
2591
|
-
if (!ok) return;
|
|
2592
|
-
}
|
|
2593
|
-
bb.ops.splice(oi, 1);
|
|
2594
|
-
if (syncPatchToMes(bi, bb.ops)) { addLog('删除变量更新语句:' + o.path); toast('已删除并写回消息(可再点 MVU 重演生效)', 'success'); renderParse(); }
|
|
2595
|
-
}));
|
|
2596
|
-
};
|
|
2597
|
-
renderParse();
|
|
2598
|
-
$('#parse-refresh', body).addEventListener('click', () => { renderParse(); addLog('重新解析变量更新'); });
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
/* ============ Tab7 设置(配置 + 小白功能说明) ============ */
|
|
2602
|
-
const HELP_TEXT = {
|
|
2603
|
-
hero: `<b>主角 = 你自己(user 角色)的养成数据。</b><br>
|
|
2604
|
-
改完任何一项,点页面底部金色「应用修改」按钮才会生效。<br>
|
|
2605
|
-
· <b>基础属性</b>:种族/等级/经验/身份职业等。等级范围 1~25,改太高游戏会失衡。<br>
|
|
2606
|
-
· <b>五维属性</b>:力量/敏捷/体质/智力/精神,影响战斗与检定。<br>
|
|
2607
|
-
· <b>资源与货币</b>:生命值(HP)/法力值(MP)/体力值(SP)及上限(v4.3.1 拆为 当前/基础(只读)/额外,总上限 = 基础+额外);金钱是日常货币;命运点数(FP)是命定系统的抽卡/复活货币。<br>
|
|
2608
|
-
· <b>状态效果</b>:增益/减益 buff,如「二阶基因锁」。<br>
|
|
2609
|
-
· <b>背包/装备/技能</b>:点物品名可展开编辑细节,右侧按钮可添加或删除。<br>
|
|
2610
|
-
· <b>登神长阶</b>:成神系统,等级达标后才会开放(12级开要素、16级权能、20级法则、24级神位、25级神国)。<br>
|
|
2611
|
-
<b>注意</b>:删除操作会二次确认;所有修改都能在「备份·历史·注入」Tab 回滚。`,
|
|
2612
|
-
npc: `<b>NPC = 关系列表里的角色(同伴/敌人)。</b><br>
|
|
2613
|
-
顶部下拉选择角色,改完点「应用修改」。<br>
|
|
2614
|
-
· <b>基础信息</b>:等级、好感度(-100~100,影响互动态度)、种族、在场(是否出现在当前剧情)、命定契约(是否缔结誓约)。<br>
|
|
2615
|
-
· <b>五维/状态/背包/装备/技能</b>:与主角同理。<br>
|
|
2616
|
-
· <b>扮演资料</b>:性格/外貌/心里话/背景故事,直接改文本即可让角色人设变化。<br>
|
|
2617
|
-
· <b>新增/删除 NPC</b>:绿色 + 按钮新增,垃圾桶按钮删除整个角色。<br>
|
|
2618
|
-
<b>提示</b>:好感度单次变化建议 ±5 以内,避免剧情突兀。`,
|
|
2619
|
-
world: `<b>世界 = 剧情环境数据。</b><br>
|
|
2620
|
-
· <b>时间</b>:纪元/年/月/日/星期/时/分,按「复兴纪元488年4月20日 星期一-15:00」格式重组。<br>
|
|
2621
|
-
· <b>地点</b>:7 层地理格式:大陆-区域-势力-子势力-聚落-区位-详细位置。<br>
|
|
2622
|
-
· <b>新闻</b>:三栏共 15 条(快报/留言板/茶会),空新闻会触发全量刷新,建议保持 80 字以上。<br>
|
|
2623
|
-
· <b>任务列表</b>:点任务名展开编辑 状态/关注度/进展/详情/目标/奖励。<br>
|
|
2624
|
-
· <b>事件</b>:开关/标题/阶段/信号,信号一般由系统管理,手动改需谨慎。`,
|
|
2625
|
-
cal: `<b>日历 = date 变量的日程与统计。</b><br>
|
|
2626
|
-
· <b>date.npcs</b>:所有 NPC 的等级/经验(含不在关系列表里的临时角色)。<br>
|
|
2627
|
-
· <b>date.log</b>:死亡次数、累计获得命运点等统计。<br>
|
|
2628
|
-
· <b>配置开关</b>:NPC 是否随玩家升级、是否需契约才得经验等。<br>
|
|
2629
|
-
· <b>known_tags</b>:日历事件标签白名单,用逗号分隔。`,
|
|
2630
|
-
global: `<b>全局 = 聊天级模板变量(setvar)。</b><br>
|
|
2631
|
-
这些是角色的「写作风格开关」:系统核心、爆料风格、字数要求、人称、对话占比等。<br>
|
|
2632
|
-
点变量名展开编辑,改动立即保存(无需点应用)。新增/删除用下方按钮。`,
|
|
2633
|
-
ops: `<b>操作 = 正文注入操作区。</b><br>
|
|
2634
|
-
· <b>正文注入</b>:「生成并注入差异指令」把变化的字段语句合并进最后一条 AI 回复的变量更新栏;「触发 MVU 重新处理」让引擎应用。<br>
|
|
2635
|
-
· <b>备份 / 历史 / 注入记录 / 变量解读</b>:已各自独立成 Tab,见对应页面。<br>
|
|
2636
|
-
· <b>写入方式完整说明与小白操作</b>:见「设置」Tab → 写入方式·小白操作说明。`
|
|
2637
|
-
};
|
|
2638
|
-
|
|
2639
|
-
// UI 主题模式(localStorage 记忆)
|
|
2640
|
-
let uiMode = 'status';
|
|
2641
|
-
try { const um = store.get('__md_ui_mode'); if (um === 'dark' || um === 'light' || um === 'status') uiMode = um; } catch (e) { }
|
|
2642
|
-
function setUiMode(m) {
|
|
2643
|
-
uiMode = (m === 'dark' || m === 'light' || m === 'status') ? m : 'status';
|
|
2644
|
-
try { store.set('__md_ui_mode', uiMode); } catch (e) { }
|
|
2645
|
-
applyTheme();
|
|
2646
|
-
render();
|
|
2647
|
-
}
|
|
2648
|
-
// 应用主题:status=跟随状态栏 --theme-*;dark=CSS 默认深色;light=浅色预设
|
|
2649
|
-
function applyTheme() {
|
|
2650
|
-
const rootEl = $('#md-modifier-root', P.document);
|
|
2651
|
-
if (!rootEl) return;
|
|
2652
|
-
const VARS = ['--md-bg0', '--md-bg1', '--md-bg2', '--md-line', '--md-line2', '--md-tx', '--md-tx2', '--md-accent', '--md-ok', '--md-danger'];
|
|
2653
|
-
VARS.forEach(k => { try { rootEl.style.removeProperty(k); } catch (e) { } });
|
|
2654
|
-
if (uiMode === 'dark') return;
|
|
2655
|
-
if (uiMode === 'light') {
|
|
2656
|
-
const lm = { '--md-bg0': '#f2f3f7', '--md-bg1': '#ffffff', '--md-bg2': '#e9ebf2', '--md-line': '#d4d8e2', '--md-line2': '#c4c9d6', '--md-tx': '#1e2230', '--md-tx2': '#6b7285', '--md-accent': '#b8860b', '--md-ok': '#2e7d32', '--md-danger': '#c62828' };
|
|
2657
|
-
Object.entries(lm).forEach(([k, v]) => { try { rootEl.style.setProperty(k, v); } catch (e) { } });
|
|
2658
|
-
return;
|
|
2659
|
-
}
|
|
2660
|
-
// status:读状态栏主题根
|
|
2661
|
-
const themeEl = P.document.querySelector('[class*="settingsTabTheme"]');
|
|
2662
|
-
if (!themeEl) return;
|
|
2663
|
-
const cs = P.getComputedStyle ? P.getComputedStyle(themeEl) : null;
|
|
2664
|
-
if (!cs) return;
|
|
2665
|
-
const g = (name) => { try { return (cs.getPropertyValue(name) || '').trim(); } catch (e) { return ''; } };
|
|
2666
|
-
const tx1 = g('--theme-primary-text');
|
|
2667
|
-
const map = {
|
|
2668
|
-
'--md-bg0': g('--theme-overlay-bg') || g('--theme-card-bg'),
|
|
2669
|
-
'--md-bg1': g('--theme-card-bg') || g('--theme-content-bg') || g('--theme-primary-bg'),
|
|
2670
|
-
'--md-bg2': g('--theme-primary-bg') || g('--theme-card-bg'),
|
|
2671
|
-
'--md-line': g('--theme-card-border') || g('--theme-primary-bg'),
|
|
2672
|
-
'--md-line2': g('--theme-card-border') || g('--theme-primary-bg'),
|
|
2673
|
-
'--md-tx': tx1,
|
|
2674
|
-
'--md-accent': g('--theme-ascension-element') || g('--theme-currency-gold')
|
|
2675
|
-
};
|
|
2676
|
-
if (tx1) {
|
|
2677
|
-
map['--md-tx2'] = 'color-mix(in srgb, ' + tx1 + ' 78%, transparent)';
|
|
2678
|
-
}
|
|
2679
|
-
Object.entries(map).forEach(([md, v]) => { if (v) { try { rootEl.style.setProperty(md, v); } catch (e) { } } });
|
|
2680
|
-
}
|
|
2681
|
-
// UI Tab:主题模式选择
|
|
2682
|
-
function renderUi(body) {
|
|
2683
|
-
const MODES = { status: '跟随状态栏 UI', dark: '预设 · 深色', light: '预设 · 浅色' };
|
|
2684
|
-
let html = '';
|
|
2685
|
-
html += section('主题外观', 'palette', `
|
|
2686
|
-
<div class="md-note">选择修改器面板的配色:<b>跟随状态栏 UI</b> 自动取状态栏主题色;<b>预设</b> 为固定的深色/浅色方案(状态栏无主题或想固定外观时使用)。</div>
|
|
2687
|
-
<div class="md-row"><label style="width:88px">UI 主题</label>
|
|
2688
|
-
<select id="md-ui-mode">${Object.entries(MODES).map(([k, v]) => `<option value="${k}" ${uiMode === k ? 'selected' : ''}>${v}</option>`).join('')}</select></div>
|
|
2689
|
-
<div class="md-note">切换后立即生效并记忆(下次打开保持)。</div>
|
|
2690
|
-
`, true);
|
|
2691
|
-
body.insertAdjacentHTML('beforeend', html);
|
|
2692
|
-
bindFoldCommon(body);
|
|
2693
|
-
$('#md-ui-mode', body).addEventListener('change', e => setUiMode(e.target.value));
|
|
2694
|
-
}
|
|
2695
|
-
// 帮助 Tab:快速上手 + 各 Tab 功能 + 常见问题
|
|
2696
|
-
function renderHelp(body) {
|
|
2697
|
-
let html = '';
|
|
2698
|
-
html += section('快速上手(小白指南)', 'question', `
|
|
2699
|
-
<div class="md-note">① 在「主角 / NPC / 世界 / 日历 / 全局 / UI」等 Tab 修改数值;<br>
|
|
2700
|
-
② 点底部「保存当前修改」→ 操作 Tab「待提交修改」可检查/调整;<br>
|
|
2701
|
-
③ 点「生成并注入差异指令」(正文注入,推荐)或「应用修改」(直接写入)应用,应用后自动清空;<br>
|
|
2702
|
-
④ 想留档:备份 Tab「导出快照」下载 JSON,「历史回滚」可回退;<br>
|
|
2703
|
-
⑤ 想看本轮回复改了哪些变量:切到「变量解读」Tab;面板外观在「UI」Tab 调整。</div>`, true);
|
|
2704
|
-
html += section('各 Tab 功能', 'gear', `
|
|
2705
|
-
<div class="md-note">· <b>主角</b>:基础/资源(金钱/命运点/属性点)、五维、状态效果(点击展开编辑 类型/效果/层数/剩余时间/来源)、背包/装备/技能(点击条目行下方展开逐项编辑);<br>
|
|
2706
|
-
· <b>NPC</b>:关系列表角色下拉选择/新增/删除;基础信息(种族/等级[同步 date.npcs]/经验[写 date.npcs]/好感度/在场/命定契约)、五维、状态/背包/装备/技能、扮演资料;<br>
|
|
2707
|
-
· <b>世界</b>:时间(纪元/年月日/星期/时分重组)、地点、新闻三栏、任务(点击行展开编辑)、事件(开关/标题/阶段/信号/已完成 + 扩展变量);<br>
|
|
2708
|
-
· <b>日历</b>:date.npcs 等级/经验/升级所需、date.log 统计、date 配置开关、known_tags、扩展变量(date 二创字段);<br>
|
|
2709
|
-
· <b>全局</b>:chat_metadata.variables 模板变量(点击行展开编辑值、新增/删除);<br>
|
|
2710
|
-
· <b>操作</b>:待提交修改(保存后在此检查/编辑/删除)、正文注入操作、直接写入、重新载入;<br>
|
|
2711
|
-
· <b>备份·历史·注入</b>:修改历史回滚、快照导出、注入记录;<br>
|
|
2712
|
-
· <b>变量解读</b>:解析最后 AI 消息的变量更新语句(点击 ✎ 编辑 / ✕ 删除并写回消息)+ 未声明变化检测;<br>
|
|
2713
|
-
· <b>UI</b>:面板主题(跟随状态栏 / 预设深/浅);<br>
|
|
2714
|
-
· <b>设置</b>:写入方式、确认弹窗、自动保存等。</div>`, false);
|
|
2715
|
-
html += section('常见问题', 'question', `
|
|
2716
|
-
· <b>入口没出现?</b> 确认修改器脚本已启用;若刚导入需重新进入聊天。<br>
|
|
2717
|
-
· <b>正文注入提示无变化?</b> 日历/快照类改动(如 NPC 等级/经验)请用「应用修改」(直接写入)。<br>
|
|
2718
|
-
· <b>修改不生效?</b> 点「保存当前修改」后再应用;正文注入后需点「MVU 重演」让引擎处理。<br>
|
|
2719
|
-
· <b>想放弃修改?</b> 重新载入变量 或 备份 Tab 历史回滚。`, false);
|
|
2720
|
-
body.insertAdjacentHTML('beforeend', html);
|
|
2721
|
-
bindFoldCommon(body);
|
|
2722
|
-
}
|
|
2723
|
-
function renderSettings(body) {
|
|
2724
|
-
let html = '';
|
|
2725
|
-
html += sectionHelp('写入方式(选择修改生效的途径)', 'gear',
|
|
2726
|
-
`<b>安全性排序:正文 > 直接</b><br>
|
|
2727
|
-
· <b>正文注入(默认,推荐)</b>:单条差异语句合并进最后一条 AI 回复的变量更新栏,点「MVU 重演」由引擎校验应用——安全且本轮即时生效。<br>
|
|
2728
|
-
· <b>直接写入(最直接,谨慎使用)</b>:点应用立刻写入聊天变量,不经过引擎校验,最快但可能绕过规则约束。`,
|
|
2729
|
-
`<div class="md-cfgrow"><label>写入方式</label>
|
|
2730
|
-
<select id="cfg-writeMode">
|
|
2731
|
-
<option value="body" ${cfg.writeMode === 'body' ? 'selected' : ''}>正文注入(默认·推荐)</option>
|
|
2732
|
-
<option value="direct" ${cfg.writeMode === 'direct' ? 'selected' : ''}>直接写入(谨慎)</option>
|
|
2733
|
-
</select></div>
|
|
2734
|
-
<div class="md-note" style="margin-top:4px">当前生效方式会显示在各页面的「应用修改」按钮上。AI 生成回复期间禁止任何修改。</div>`);
|
|
2735
|
-
html += section('写入方式·小白操作说明', 'edit', `
|
|
2736
|
-
<div style="font-size:12px;line-height:1.8;color:var(--md-tx,#cdd6ea)">
|
|
2737
|
-
<b style="color:var(--md-accent,#ffd27a)">① 正文注入(默认·推荐)</b><br>
|
|
2738
|
-
操作:改好变量 → 点「应用修改」→ 再点「MVU 重演」按钮(或角色卡 MVU 面板的「重新处理变量」)。<br>
|
|
2739
|
-
原理:修改器只把<b>变化的字段语句</b>(如 <code>{ "op": "replace", "path": "/主角/金钱", ... }</code>)合并进最后一条 AI 回复的「变量更新栏」,由角色卡引擎校验应用——走引擎流程、不浪费上下文 token、本轮即时生效。<br><br>
|
|
2740
|
-
<b style="color:#ff8fa8">② 直接写入(最直接·谨慎)</b><br>
|
|
2741
|
-
操作:改好变量 → 点「应用修改」,立即生效。<br>
|
|
2742
|
-
原理:直接把工作区变量写回聊天变量并同步 MVU 引擎,不经引擎 schema 校验,最快但可能绕过角色卡的规则约束(如登神长阶解锁条件)。<br><br>
|
|
2743
|
-
<b>共同点</b>:所有修改先进入内存工作区;写入前自动生成回滚点(「历史」Tab 可回滚);删除与恢复都有二次确认。
|
|
2744
|
-
</div>
|
|
2745
|
-
`, false);
|
|
2746
|
-
html += section('安全性', 'shield', `
|
|
2747
|
-
<div class="md-cfgrow"><label>应用修改前二次确认</label><input type="checkbox" id="cfg-confirmApply" ${cfg.confirmApply ? 'checked' : ''}></div>
|
|
2748
|
-
<div class="md-cfgrow"><label>删除条目前二次确认</label><input type="checkbox" id="cfg-confirmDelete" ${cfg.confirmDelete ? 'checked' : ''}></div>
|
|
2749
|
-
<div class="md-cfgrow"><label>应用后同步 MVU 引擎(推荐)</label><input type="checkbox" id="cfg-syncMvu" ${cfg.syncMvu ? 'checked' : ''}></div>
|
|
2750
|
-
<div class="md-cfgrow"><label>应用后自动保存聊天(推荐)</label><input type="checkbox" id="cfg-autoSave" ${cfg.autoSave ? 'checked' : ''}></div>
|
|
2751
|
-
<div class="md-note">安全说明:所有修改先进入内存工作区,只有点「应用修改」才写入聊天;写入前自动生成回滚点,可在「历史」Tab 一键恢复。AI 生成回复期间禁止修改(自动检测)。</div>
|
|
2752
|
-
`, false);
|
|
2753
|
-
html += section('界面', 'palette', `
|
|
2754
|
-
<div class="md-cfgrow"><label>记住窗口位置与大小</label><input type="checkbox" id="cfg-rememberPos" ${cfg.rememberPos ? 'checked' : ''}></div>
|
|
2755
|
-
<div class="md-cfgrow"><label>回滚历史条数上限</label><input type="number" id="cfg-maxHistory" value="${esc(cfg.maxHistory)}" min="1" max="100" style="width:70px"></div>
|
|
2756
|
-
<div class="md-note">窗口可拖标题栏移动、拖右下角手柄缩放;标题栏 ⟳ 按钮可随时刷新快照。</div>
|
|
2757
|
-
`, false);
|
|
2758
|
-
html += section('功能说明(小白必读)', 'edit', Object.entries(HELP_TEXT).map(([k, v]) =>
|
|
2759
|
-
`<div class="md-cfgrow" style="align-items:flex-start"><label style="flex:0 0 44px">${({ hero: '主角', npc: 'NPC', world: '世界', cal: '日历', global: '全局', ops: '操作' })[k]}</label>
|
|
2760
|
-
<span style="flex:1;font-size:11px;color:var(--md-tx2,#9aa1b5);line-height:1.7;white-space:normal">${v}</span></div>`
|
|
2761
|
-
).join(''), false);
|
|
2762
|
-
html += `<div style="padding:6px 0;display:flex;gap:6px;justify-content:flex-end">
|
|
2763
|
-
<button class="md-btn" id="md-cfg-reset">${ICONS.refresh} 恢复默认设置</button>
|
|
2764
|
-
</div>`;
|
|
2765
|
-
body.innerHTML = html;
|
|
2766
|
-
|
|
2767
|
-
$$('.md-help', body).forEach(h => h.addEventListener('click', e => {
|
|
2768
|
-
e.stopPropagation();
|
|
2769
|
-
const box = h.parentElement.nextElementSibling;
|
|
2770
|
-
if (box && box.classList.contains('md-helpbox')) box.classList.toggle('show');
|
|
2771
|
-
}));
|
|
2772
|
-
bindFoldCommon(body);
|
|
2773
|
-
|
|
2774
|
-
const bindCfg = (id, fn) => { const el = $('#cfg-' + id, body); el && el.addEventListener('change', () => fn(el)); };
|
|
2775
|
-
bindCfg('writeMode', el => { cfg.writeMode = el.value; saveCfg(); addLog('写入方式切换为:' + el.value); toast('写入方式已切换', 'success'); });
|
|
2776
|
-
bindCfg('confirmApply', el => { cfg.confirmApply = el.checked; saveCfg(); });
|
|
2777
|
-
bindCfg('confirmDelete', el => { cfg.confirmDelete = el.checked; saveCfg(); });
|
|
2778
|
-
bindCfg('syncMvu', el => { cfg.syncMvu = el.checked; saveCfg(); });
|
|
2779
|
-
bindCfg('autoSave', el => { cfg.autoSave = el.checked; saveCfg(); });
|
|
2780
|
-
bindCfg('rememberPos', el => { cfg.rememberPos = el.checked; saveCfg(); });
|
|
2781
|
-
bindCfg('maxHistory', el => { cfg.maxHistory = Math.max(1, Math.min(100, num(el.value) || 20)); saveCfg(); });
|
|
2782
|
-
$('#md-cfg-reset', body).addEventListener('click', () => {
|
|
2783
|
-
cfg = Object.assign({}, DEFAULT_CFG);
|
|
2784
|
-
saveCfg();
|
|
2785
|
-
addLog('恢复默认设置');
|
|
2786
|
-
renderSettings(body);
|
|
2787
|
-
toast('已恢复默认设置', 'success');
|
|
2788
|
-
});
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
/* ============ Tab 关于(版本信息 + 更新日志) ============ */
|
|
2792
|
-
function renderAbout(body) {
|
|
2793
|
-
const LOG = [
|
|
2794
|
-
['v2.13', '修复:跟随状态栏 UI 主题映射改用正确 CSS 变量(--theme-primary-text/--theme-card-bg 等)+color-mix;卡片折叠失效(bindFoldCommon 幂等防重复监听);新增 Tab 切换后滚动位置记忆(上下 .md-body + 左右 .md-tabs)。'],
|
|
2795
|
-
['v2.12', '新增:开局检测 + 转生点修改(creator-app reincarnationPoints,读写走 Pinia store.character / updateCharacterField);手机端触屏拖拽(pointer 事件)+ UI 尺寸适配(移动端铺满/单列/全屏按钮);tab 重排合并(操作含备份·历史·注入,设置含 UI+帮助,新增「关于」)。'],
|
|
2796
|
-
['v2.11', '适配 v4.3.1:资源值(当前/基础上限只读/额外上限)双向兼容读写、资产编辑、NPC 资源/资产、事件信号保护提示、冒险者等级新版隐藏;反向兼容 v4.2.1 存档。'],
|
|
2797
|
-
['v2.10', '修复悬浮球不可拖动(新增 fab 拖拽)+ 悬浮球/窗口位置双向同步(拖 fab 窗口跟随、拖窗口 fab 跟随、恢复一致)。']
|
|
2798
|
-
];
|
|
2799
|
-
let html = '';
|
|
2800
|
-
html += section('版本信息', 'wand', `
|
|
2801
|
-
<div class="md-note">命定之诗 · 全量修改器 <b style="color:var(--md-accent,#ffd27a)">${VER}</b>(酒馆助手版·tavern_helper)
|
|
2802
|
-
<br>直接读写 MVU 消息变量(stat_data / date / calendar_float_store)+ 模板变量(setvar),并提供正文注入/直接写入两种应用方式。</div>
|
|
2803
|
-
`, true);
|
|
2804
|
-
html += section('更新日志', 'calendar', LOG.map(([v, d]) =>
|
|
2805
|
-
`<div class="md-item" style="display:block"><b style="color:var(--md-accent,#ffd27a)">${esc(v)}</b><br><span style="color:var(--md-tx2,#9aa1b5);font-size:11px;line-height:1.7">${esc(d)}</span></div>`
|
|
2806
|
-
).join(''), true);
|
|
2807
|
-
html += section('技术说明', 'edit', `
|
|
2808
|
-
<div class="md-note">· <b>入口</b>:状态栏设置 Tab / 悬浮球。<br>
|
|
2809
|
-
· <b>写入方式</b>:正文注入(默认,走 MVU 引擎)+ 直接写入(不经校验)。<br>
|
|
2810
|
-
· <b>转生点</b>:属 creator-app 前端态(Pinia store),非 stat_data 变量。<br>
|
|
2811
|
-
· <b>反向兼容</b>:自动识别 v4.2.1 单值资源结构(旧档)与 v4.3.1 嵌套结构(新档)。</div>
|
|
2812
|
-
`, false);
|
|
2813
|
-
body.innerHTML = html;
|
|
2814
|
-
bindFoldCommon(body);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
/* ============ 启动 ============ */
|
|
2818
|
-
function init() {
|
|
2819
|
-
if (P.document.getElementById('md-modifier-root')) return;
|
|
2820
|
-
loadCfg();
|
|
2821
|
-
injectStyle();
|
|
2822
|
-
buildPanel();
|
|
2823
|
-
// 立即加载(readLatestVar 直接读 chat,不依赖 MVU;MVU 仅作兜底)
|
|
2824
|
-
loadAll();
|
|
2825
|
-
render();
|
|
2826
|
-
// 若此时 MVU 尚未就绪,稍后补一次加载
|
|
2827
|
-
setTimeout(() => { loadAll(); render(); }, 2500);
|
|
2828
|
-
// 聊天切换时刷新数据
|
|
2829
|
-
const es = getEventSource();
|
|
2830
|
-
const et = getEventTypes();
|
|
2831
|
-
if (es && et) {
|
|
2832
|
-
es.on(et.CHAT_CHANGED, () => { loadAll(); });
|
|
2833
|
-
es.on(et.MESSAGE_SENT, () => { loadAll(); });
|
|
2834
|
-
if (et.MESSAGE_UPDATED) es.on(et.MESSAGE_UPDATED, () => { loadAll(); });
|
|
2835
|
-
if (et.GENERATION_STARTED) es.on(et.GENERATION_STARTED, () => { loadAll(); });
|
|
2836
|
-
}
|
|
2837
|
-
// 面板打开时强制重载
|
|
2838
|
-
const fab = P.document.getElementById('md-modifier-fab');
|
|
2839
|
-
if (fab) fab.addEventListener('click', () => { loadAll(); });
|
|
2840
|
-
// 轮询兜底 + 实时刷新:每 8s 对比聊天最新 stat_data 与工作区,变化则自动重载(面板打开时可见即更新)
|
|
2841
|
-
let lastChatLen = getChatArray().length;
|
|
2842
|
-
let lastSdJson = current.stat_data ? JSON.stringify(current.stat_data) : '';
|
|
2843
|
-
setInterval(() => {
|
|
2844
|
-
const n = getChatArray().length;
|
|
2845
|
-
const latest = readLatestVar('stat_data');
|
|
2846
|
-
const latestJson = latest ? JSON.stringify(latest) : '';
|
|
2847
|
-
if (n !== lastChatLen) { lastChatLen = n; lastSdJson = latestJson; loadAll(); if (root && root.style.display !== 'none') render(); }
|
|
2848
|
-
else if (latestJson && latestJson !== lastSdJson && !isGenerating()) {
|
|
2849
|
-
lastSdJson = latestJson;
|
|
2850
|
-
loadAll();
|
|
2851
|
-
if (root && root.style.display !== 'none') render();
|
|
2852
|
-
}
|
|
2853
|
-
else if (!current.stat_data && getChatArray().length > 0) { loadAll(); }
|
|
2854
|
-
}, 8000);
|
|
2855
|
-
console.log('[' + NAME + '] 已启动');
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
if (P.document.readyState === 'loading') {
|
|
2859
|
-
P.document.addEventListener('DOMContentLoaded', init);
|
|
2860
|
-
} else {
|
|
2861
|
-
init();
|
|
2862
|
-
}
|
|
2863
|
-
})();
|