gmweb:游戏内一键换装、方案配置与全局热键说明
This commit is contained in:
1 parent
3b240ec176
commit
cc546031b0
5 files changed
+586
-3
No files matched your search
@@ -0,0 +1,386 @@
|
||||
/* 一键换装(游戏内 GM 窗口)
|
||||
* IE11 兼容:ES5 + XMLHttpRequest,不使用箭头函数/模板字符串/flex。
|
||||
* 数据:/api/loadout/config(本地 config.json),角色/装备/应用走 serve.py 代理到 18111。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var S = {
|
||||
cfg: null,
|
||||
chars: [],
|
||||
charName: '',
|
||||
page: 0,
|
||||
scheme: -1,
|
||||
equipment: [],
|
||||
cooldown: {},
|
||||
timer: null
|
||||
};
|
||||
|
||||
function keyText(code) {
|
||||
if (code >= 112 && code <= 123) { return 'F' + (code - 111); }
|
||||
if (code >= 96 && code <= 105) { return 'Num' + (code - 96); }
|
||||
if (code >= 48 && code <= 57) { return String.fromCharCode(code); }
|
||||
if (code >= 65 && code <= 90) { return String.fromCharCode(code); }
|
||||
if (code === 13) { return 'Enter'; }
|
||||
if (code === 32) { return 'Space'; }
|
||||
return 'VK' + code;
|
||||
}
|
||||
|
||||
function modText(mods) {
|
||||
var out = [];
|
||||
if (mods & 2) { out.push('Ctrl'); }
|
||||
if (mods & 1) { out.push('Alt'); }
|
||||
if (mods & 4) { out.push('Shift'); }
|
||||
return out;
|
||||
}
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function log(text, cls) {
|
||||
var box = $('log');
|
||||
if (!box) { return; }
|
||||
var d = document.createElement('div');
|
||||
if (cls) { d.className = cls; }
|
||||
var t = new Date();
|
||||
var p = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||
d.innerHTML = '[' + p(t.getHours()) + ':' + p(t.getMinutes()) + ':' + p(t.getSeconds()) + '] ' + esc(text);
|
||||
box.appendChild(d);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
while (box.childNodes.length > 120) { box.removeChild(box.firstChild); }
|
||||
}
|
||||
|
||||
function api(path, method, body, cb) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(method || 'GET', '/api' + path, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState !== 4) { return; }
|
||||
if (xhr.status === 0) {
|
||||
log('连不上本地代理(gmweb/serve.py 未启动?)', 'err');
|
||||
if (cb) { cb(null); }
|
||||
return;
|
||||
}
|
||||
var data = null;
|
||||
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
|
||||
if (!data) {
|
||||
log('响应不是 JSON:' + String(xhr.responseText || '').substring(0, 120), 'err');
|
||||
if (cb) { cb(null); }
|
||||
return;
|
||||
}
|
||||
if (data.ok === false || data.success === false) {
|
||||
log('失败:' + (data.error || data.message || '未知错误'), 'err');
|
||||
if (cb) { cb(null); }
|
||||
return;
|
||||
}
|
||||
if (cb) { cb(data); }
|
||||
};
|
||||
xhr.send(body ? JSON.stringify(body) : null);
|
||||
}
|
||||
|
||||
function msg(text) {
|
||||
var el = $('lo-msg');
|
||||
if (el) { el.innerHTML = esc(text || ''); }
|
||||
}
|
||||
|
||||
function currentPage() {
|
||||
if (!S.cfg || !S.cfg.Pages || !S.cfg.Pages.length) { return null; }
|
||||
if (S.page < 0 || S.page >= S.cfg.Pages.length) { S.page = 0; }
|
||||
return S.cfg.Pages[S.page];
|
||||
}
|
||||
|
||||
function normalizeConfig(data) {
|
||||
if (!data || !data.Pages || !data.Pages.length) {
|
||||
data = { ServerUrl: 'http://127.0.0.1:18111', AdminToken: '', ActivePage: 0,
|
||||
Pages: [{ Name: '默认页', Schemes: [] }] };
|
||||
}
|
||||
if (!data.Pages[0].Schemes) { data.Pages[0].Schemes = []; }
|
||||
S.page = parseInt(data.ActivePage, 10) || 0;
|
||||
if (S.page < 0 || S.page >= data.Pages.length) { S.page = 0; }
|
||||
return data;
|
||||
}
|
||||
|
||||
function loadHotkeys() {
|
||||
api('/loadout/hotkeys', 'GET', null, function (d) {
|
||||
var el = $('lo-hotkeys');
|
||||
if (!el) { return; }
|
||||
if (!d || !d.data || !(d.data.schemes || []).length) {
|
||||
el.innerHTML = '全局热键:当前页没有配置快捷键';
|
||||
return;
|
||||
}
|
||||
var parts = [];
|
||||
var schemes = d.data.schemes;
|
||||
for (var i = 0; i < schemes.length; i++) {
|
||||
var sc = schemes[i];
|
||||
var mods = modText(sc.modifiers || 0);
|
||||
mods.push(keyText(sc.keycode));
|
||||
parts.push(esc(sc.name || '') + ' → ' + esc(mods.join('+')));
|
||||
}
|
||||
el.innerHTML = '全局热键(当前页):' + parts.join(' ');
|
||||
});
|
||||
}
|
||||
|
||||
function loadConfig(cb) {
|
||||
api('/loadout/config', 'GET', null, function (d) {
|
||||
if (!d) { msg('读取换装配置失败'); if (cb) { cb(false); } return; }
|
||||
S.cfg = normalizeConfig(d.data || {});
|
||||
renderPages();
|
||||
renderSchemes();
|
||||
loadHotkeys();
|
||||
if (cb) { cb(true); }
|
||||
});
|
||||
}
|
||||
|
||||
function saveConfig(cb) {
|
||||
if (!S.cfg) { return; }
|
||||
S.cfg.ActivePage = S.page;
|
||||
api('/loadout/config', 'POST', S.cfg, function (d) {
|
||||
if (d) { msg('换装配置已保存'); log('换装配置已保存', 'ok'); loadHotkeys(); }
|
||||
if (cb) { cb(!!d); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderPages() {
|
||||
var sel = $('lo-page');
|
||||
if (!sel) { return; }
|
||||
var pages = (S.cfg && S.cfg.Pages) || [];
|
||||
var h = [];
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
h.push('<option value="' + i + '">' + esc(pages[i].Name || ('页 ' + (i + 1))) + '</option>');
|
||||
}
|
||||
sel.innerHTML = h.join('');
|
||||
sel.value = String(S.page);
|
||||
}
|
||||
|
||||
function renderSchemes() {
|
||||
var box = $('lo-schemes');
|
||||
if (!box) { return; }
|
||||
var page = currentPage();
|
||||
var schemes = (page && page.Schemes) || [];
|
||||
if (!schemes.length) {
|
||||
box.innerHTML = '<div class="lo-empty">当前页还没有方案</div>';
|
||||
S.scheme = -1;
|
||||
renderDetail();
|
||||
return;
|
||||
}
|
||||
var h = [];
|
||||
for (var i = 0; i < schemes.length; i++) {
|
||||
var sc = schemes[i];
|
||||
h.push('<div class="li' + (i === S.scheme ? ' on' : '') + '" data-i="' + i + '">' +
|
||||
'<span class="nm">' + esc(sc.Name || ('方案 ' + (i + 1))) + '</span>' +
|
||||
'<span class="lo-scheme-meta">' + esc(sc.CharacterName || '') +
|
||||
' · ' + ((sc.Slots || []).length) + ' 件</span></div>');
|
||||
}
|
||||
box.innerHTML = h.join('');
|
||||
var lis = box.getElementsByClassName('li');
|
||||
for (var k = 0; k < lis.length; k++) {
|
||||
lis[k].onclick = function () {
|
||||
S.scheme = parseInt(this.getAttribute('data-i'), 10);
|
||||
renderSchemes();
|
||||
};
|
||||
}
|
||||
if (S.scheme < 0 || S.scheme >= schemes.length) { S.scheme = 0; }
|
||||
var active = box.getElementsByClassName('li')[S.scheme];
|
||||
if (active) { active.className = 'li on'; }
|
||||
renderDetail();
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
var page = currentPage();
|
||||
var schemes = (page && page.Schemes) || [];
|
||||
var detail = $('lo-detail');
|
||||
var slotsBox = $('lo-slots');
|
||||
if (!detail || !slotsBox) { return; }
|
||||
var sc = schemes[S.scheme];
|
||||
if (!sc) {
|
||||
detail.innerHTML = '未选择方案';
|
||||
slotsBox.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
detail.innerHTML = '<b>' + esc(sc.Name || '') + '</b>' +
|
||||
'<span class="lo-scheme-meta">' + esc(sc.CharacterName || '未绑定角色') + '</span>';
|
||||
var slots = sc.Slots || [];
|
||||
if (!slots.length) {
|
||||
slotsBox.innerHTML = '<div class="lo-empty">该方案没有保存装备</div>';
|
||||
return;
|
||||
}
|
||||
var h = [];
|
||||
for (var i = 0; i < slots.length; i++) {
|
||||
h.push('<div class="lo-slot">部位 <b>' + esc(slots[i].slot) + '</b>' +
|
||||
'<span class="id">ID ' + esc(slots[i].item_id) + '</span></div>');
|
||||
}
|
||||
slotsBox.innerHTML = h.join('');
|
||||
}
|
||||
|
||||
function loadChars() {
|
||||
api('/loadout/characters', 'GET', null, function (d) {
|
||||
if (!d) { msg('角色读取失败,确认服务端已启动'); return; }
|
||||
S.chars = d.data || [];
|
||||
var sel = $('lo-char');
|
||||
if (!sel) { return; }
|
||||
var old = S.charName;
|
||||
var h = ['<option value="">选择角色</option>'];
|
||||
for (var i = 0; i < S.chars.length; i++) {
|
||||
var c = S.chars[i];
|
||||
h.push('<option value="' + esc(c.name) + '">' + esc(c.name) +
|
||||
'(Lv.' + esc(c.level) + (c.online ? ' · 在线' : '') + ')</option>');
|
||||
}
|
||||
sel.innerHTML = h.join('');
|
||||
if (old) { sel.value = old; }
|
||||
if (!sel.value && S.chars.length === 1) { sel.value = S.chars[0].name; }
|
||||
S.charName = sel.value || '';
|
||||
msg('角色 ' + S.chars.length + ' 个');
|
||||
});
|
||||
}
|
||||
|
||||
function fetchEquipment(characterName, cb) {
|
||||
api('/local/dnf-gm/loadout/equipment?character_id=' + encodeURIComponent(characterName), 'GET', null, function (d) {
|
||||
if (!d) { cb(null); return; }
|
||||
cb((d.data && d.data.slots) || []);
|
||||
});
|
||||
}
|
||||
|
||||
function saveCurrent() {
|
||||
if (!S.charName) { msg('请先选择角色'); return; }
|
||||
if (!S.cfg) { return; }
|
||||
msg('正在读取当前装备…');
|
||||
fetchEquipment(S.charName, function (slots) {
|
||||
if (!slots) { msg('读取当前装备失败'); return; }
|
||||
if (!slots.length) { msg('当前没有可保存的装备'); return; }
|
||||
var name = window.prompt('方案名称', S.charName + ' 当前装备');
|
||||
if (!name) { msg('已取消'); return; }
|
||||
var page = currentPage();
|
||||
if (!page) { return; }
|
||||
if (!page.Schemes) { page.Schemes = []; }
|
||||
page.Schemes.push({
|
||||
Name: String(name),
|
||||
CharacterName: S.charName,
|
||||
Modifiers: 0,
|
||||
KeyCode: 0,
|
||||
Slots: slots
|
||||
});
|
||||
S.scheme = page.Schemes.length - 1;
|
||||
saveConfig(function (ok) {
|
||||
if (ok) { renderSchemes(); msg('已保存方案:' + name); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cooldownRemaining(name) {
|
||||
var until = S.cooldown[name] || 0;
|
||||
return Math.max(0, Math.ceil((until - Date.now()) / 1000));
|
||||
}
|
||||
|
||||
function tickCooldown() {
|
||||
var el = $('lo-cooldown');
|
||||
var page = currentPage();
|
||||
var schemes = (page && page.Schemes) || [];
|
||||
var sc = schemes[S.scheme];
|
||||
if (!el) { return; }
|
||||
if (!sc) { el.innerHTML = ''; return; }
|
||||
var remain = cooldownRemaining(sc.CharacterName || '');
|
||||
el.innerHTML = remain > 0 ? ('换装冷却 ' + remain + ' 秒') : '';
|
||||
}
|
||||
|
||||
function applyScheme() {
|
||||
var page = currentPage();
|
||||
var schemes = (page && page.Schemes) || [];
|
||||
var sc = schemes[S.scheme];
|
||||
if (!sc) { msg('请先选择方案'); return; }
|
||||
if (!sc.CharacterName || !(sc.Slots || []).length) { msg('方案没有角色或装备'); return; }
|
||||
var remain = cooldownRemaining(sc.CharacterName);
|
||||
if (remain > 0) { msg('换装冷却中,剩余 ' + remain + ' 秒'); return; }
|
||||
msg('正在应用方案…');
|
||||
api('/local/dnf-gm/loadout/apply', 'POST', { character_id: sc.CharacterName, slots: sc.Slots }, function (d) {
|
||||
if (!d) { msg('应用失败'); return; }
|
||||
var r = d.data || {};
|
||||
var applied = (r.applied || []).length;
|
||||
var skipped = (r.skipped || []).length;
|
||||
var sec = parseInt(r.cooldown_seconds, 10) || 0;
|
||||
if (sec > 0) {
|
||||
S.cooldown[sc.CharacterName] = Date.now() + sec * 1000;
|
||||
}
|
||||
msg('应用完成:成功 ' + applied + ' 件,跳过 ' + skipped + ' 件' +
|
||||
(sec > 0 ? (',冷却 ' + sec + ' 秒') : ''));
|
||||
log('换装 ' + sc.Name + ':成功 ' + applied + ',跳过 ' + skipped, 'ok');
|
||||
tickCooldown();
|
||||
});
|
||||
}
|
||||
|
||||
function addPage() {
|
||||
var name = window.prompt('新换装页名称', '新页面');
|
||||
if (!name || !S.cfg) { return; }
|
||||
S.cfg.Pages.push({ Name: String(name), Schemes: [] });
|
||||
S.page = S.cfg.Pages.length - 1;
|
||||
S.scheme = -1;
|
||||
saveConfig(function (ok) { if (ok) { renderPages(); renderSchemes(); } });
|
||||
}
|
||||
|
||||
function renamePage() {
|
||||
var page = currentPage();
|
||||
if (!page || !S.cfg) { return; }
|
||||
var name = window.prompt('重命名换装页', page.Name || '');
|
||||
if (!name) { return; }
|
||||
page.Name = String(name);
|
||||
saveConfig(function (ok) { if (ok) { renderPages(); renderSchemes(); } });
|
||||
}
|
||||
|
||||
function deletePage() {
|
||||
if (!S.cfg || S.cfg.Pages.length <= 1) { msg('至少保留一个换装页'); return; }
|
||||
var page = currentPage();
|
||||
if (!page) { return; }
|
||||
if (!window.confirm('删除换装页「' + page.Name + '」及其方案?')) { return; }
|
||||
S.cfg.Pages.splice(S.page, 1);
|
||||
S.page = Math.max(0, S.page - 1);
|
||||
S.scheme = -1;
|
||||
saveConfig(function (ok) { if (ok) { renderPages(); renderSchemes(); } });
|
||||
}
|
||||
|
||||
function bind() {
|
||||
var sel = $('lo-page');
|
||||
if (sel) {
|
||||
sel.onchange = function () {
|
||||
S.page = parseInt(this.value, 10) || 0;
|
||||
S.scheme = -1;
|
||||
renderSchemes();
|
||||
loadHotkeys();
|
||||
};
|
||||
}
|
||||
var ch = $('lo-char');
|
||||
if (ch) { ch.onchange = function () { S.charName = this.value; }; }
|
||||
var b;
|
||||
if ((b = $('lo-refresh'))) { b.onclick = function () { loadChars(); }; }
|
||||
if ((b = $('lo-save'))) { b.onclick = saveCurrent; }
|
||||
if ((b = $('lo-apply'))) { b.onclick = applyScheme; }
|
||||
if ((b = $('lo-page-add'))) { b.onclick = addPage; }
|
||||
if ((b = $('lo-page-ren'))) { b.onclick = renamePage; }
|
||||
if ((b = $('lo-page-del'))) { b.onclick = deletePage; }
|
||||
}
|
||||
|
||||
function loadTab() {
|
||||
msg('正在读取换装配置…');
|
||||
loadConfig(function (ok) {
|
||||
if (!ok) { return; }
|
||||
loadChars();
|
||||
if (!S.timer) { S.timer = setInterval(tickCooldown, 500); }
|
||||
});
|
||||
}
|
||||
|
||||
window.LoadoutTab = loadTab;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bind);
|
||||
} else {
|
||||
bind();
|
||||
}
|
||||
|
||||
// 通过 #loadout 直接打开时,app.js 可能已经在 loadout.js 之前切过页
|
||||
if ((location.hash || '').replace('#', '') === 'loadout') {
|
||||
setTimeout(loadTab, 0);
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user