Files
90CNServer/gmweb/loadout.js
T

520 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 一键换装(游戏内 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 = '';
var emptyHotkey = $('lo-hotkey-text');
if (emptyHotkey) { emptyHotkey.innerHTML = ''; }
return;
}
detail.innerHTML = '<b>' + esc(sc.Name || '') + '</b>' +
'<span class="lo-scheme-meta">' + esc(sc.CharacterName || '未绑定角色') + '</span>';
var hotkeyTextEl = $('lo-hotkey-text');
if (hotkeyTextEl) { hotkeyTextEl.innerHTML = '快捷键:' + esc(hotkeyText(sc)); }
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 hotkeyText(sc) {
if (!sc || !sc.KeyCode) { return '未设置'; }
var parts = modText(sc.Modifiers || 0);
parts.push(keyText(sc.KeyCode));
return parts.join('+');
}
function currentScheme() {
var page = currentPage();
var schemes = (page && page.Schemes) || [];
return schemes[S.scheme] || null;
}
function setHotkey() {
var sc = currentScheme();
if (!sc) { msg('请先选择方案'); return; }
S.capture = true;
msg('请按下快捷键,Esc 取消');
var handler = function (ev) {
ev = ev || window.event;
var code = ev.keyCode || ev.which;
if (code === 27) {
S.capture = false;
document.removeEventListener('keydown', handler, true);
msg('已取消设置快捷键');
return false;
}
if (!code || code === 16 || code === 17 || code === 18) {
return false;
}
var mods = 0;
if (ev.ctrlKey) { mods |= 2; }
if (ev.altKey) { mods |= 1; }
if (ev.shiftKey) { mods |= 4; }
sc.KeyCode = code;
sc.Modifiers = mods;
S.capture = false;
document.removeEventListener('keydown', handler, true);
saveConfig(function (ok) {
if (ok) {
renderDetail();
loadHotkeys();
msg('快捷键已保存:' + hotkeyText(sc));
}
});
if (ev.preventDefault) { ev.preventDefault(); }
return false;
};
document.addEventListener('keydown', handler, true);
}
function clearHotkey() {
var sc = currentScheme();
if (!sc) { msg('请先选择方案'); return; }
sc.KeyCode = 0;
sc.Modifiers = 0;
saveConfig(function (ok) {
if (ok) {
renderDetail();
loadHotkeys();
msg('快捷键已清除');
}
});
}
function addScheme() {
var page = currentPage();
if (!page) { return; }
var name = window.prompt('方案名称', '方案' + ((page.Schemes || []).length + 1));
if (!name) { return; }
var character = window.prompt('绑定角色游戏内名称', S.charName || '') || '';
page.Schemes = page.Schemes || [];
page.Schemes.push({
Name: String(name),
CharacterName: String(character),
Modifiers: 0,
KeyCode: 0,
Slots: []
});
S.scheme = page.Schemes.length - 1;
saveConfig(function (ok) {
if (ok) {
renderSchemes();
msg('已新增方案:' + name);
}
});
}
function renameScheme() {
var sc = currentScheme();
if (!sc) { msg('请先选择方案'); return; }
var name = window.prompt('重命名方案', sc.Name || '');
if (!name) { return; }
sc.Name = String(name);
saveConfig(function (ok) {
if (ok) {
renderSchemes();
msg('方案已重命名');
}
});
}
function deleteScheme() {
var page = currentPage();
var sc = currentScheme();
if (!page || !sc) { msg('请先选择方案'); return; }
if (!window.confirm('删除方案“' + (sc.Name || '') + '”?')) { return; }
page.Schemes.splice(S.scheme, 1);
S.scheme = -1;
saveConfig(function (ok) {
if (ok) {
renderSchemes();
msg('方案已删除');
}
});
}
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;
if (S.cfg) { S.cfg.ActivePage = S.page; }
renderSchemes();
loadHotkeys();
// The launcher hotkey worker reads ActivePage from config.json. Persist
// the page switch here, otherwise a hotkey assigned on this page never
// becomes the active page's hotkey.
saveConfig(function (ok) {
if (ok) { 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; }
if ((b = $('lo-scheme-add'))) { b.onclick = addScheme; }
if ((b = $('lo-scheme-ren'))) { b.onclick = renameScheme; }
if ((b = $('lo-scheme-del'))) { b.onclick = deleteScheme; }
if ((b = $('lo-hotkey'))) { b.onclick = setHotkey; }
if ((b = $('lo-hotkey-clear'))) { b.onclick = clearHotkey; }
}
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);
}
})();