/* 一键换装(游戏内 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, '"'); } 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(''); } 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 = '
当前页还没有方案
'; S.scheme = -1; renderDetail(); return; } var h = []; for (var i = 0; i < schemes.length; i++) { var sc = schemes[i]; h.push('
' + '' + esc(sc.Name || ('方案 ' + (i + 1))) + '' + '' + esc(sc.CharacterName || '') + ' · ' + ((sc.Slots || []).length) + ' 件
'); } 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 = '' + esc(sc.Name || '') + '' + '' + esc(sc.CharacterName || '未绑定角色') + ''; var slots = sc.Slots || []; if (!slots.length) { slotsBox.innerHTML = '
该方案没有保存装备
'; return; } var h = []; for (var i = 0; i < slots.length; i++) { h.push('
部位 ' + esc(slots[i].slot) + '' + 'ID ' + esc(slots[i].item_id) + '
'); } 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 = ['']; for (var i = 0; i < S.chars.length; i++) { var c = S.chars[i]; h.push(''); } 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); } })();