启动器:移除服务端工具/支援兵/插件页面,支援兵模板独立保存
- 删除服务端工具、支援兵、插件三个页面(侧边栏入口 + HTML section) - server.py: 删除 servertool(9个)、companion/basic-attack 相关接口及常量 - native.py: 删除无人引用的 get_server_features/save_server_features/ companion_state/companion_save(保留 GM 工具依赖的 gameplay 方法) - shell.js: 删除 loadServerTool/loadCompanion 全部逻辑及状态变量 - shell.css: 清理 st-/cp- 相关样式 - templates/支援兵页面模板.md: 支援兵页面完整导出为独立模板 - 侧边栏「更新日志」更名为「游戏更新」
This commit is contained in:
1 parent
56236496a0
commit
977a4a1196
6 files changed
+410
-869
No files matched your search
+2
-496
@@ -1,19 +1,13 @@
|
||||
/* 90轮回启动器 · 单窗口页面与工具整合 */
|
||||
(function () {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const pages = ['start','gm','loadout','companion','servertool','plugins','settings','changelog'];
|
||||
const pages = ['start','gm','loadout','settings','changelog'];
|
||||
let shellGmInit = false;
|
||||
let loConfig = null, loPage = 0, loScheme = -1;
|
||||
let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null;
|
||||
let stFeatures = {}, stActivity = {}, stTab = 'global';
|
||||
let stGameplay = {};
|
||||
let stBatchPickup = true;
|
||||
let hotkeyCapture = false;
|
||||
let stRaidTab = 'anton';
|
||||
let stFeatureTab = 'instance.json';
|
||||
let stRawText = '';
|
||||
|
||||
const crumb = {start:'开始 / 本地服务端',gm:'GM工具',loadout:'工具 / 一键换装',companion:'工具 / 支援兵',servertool:'工具 / 服务端工具',plugins:'插件',settings:'设置',changelog:'更新日志'};
|
||||
const crumb = {start:'开始 / 本地服务端',gm:'GM工具',loadout:'工具 / 一键换装',settings:'设置',changelog:'更新日志'};
|
||||
|
||||
function shellGo(page) {
|
||||
if (!pages.includes(page)) page = 'start';
|
||||
@@ -22,8 +16,6 @@
|
||||
$('shellCrumb').textContent = crumb[page] || page;
|
||||
if (page === 'start') loadStart();
|
||||
if (page === 'loadout') loadLoadout();
|
||||
if (page === 'companion') loadCompanion();
|
||||
if (page === 'servertool') loadServerTool();
|
||||
if (page === 'settings') loadSettings();
|
||||
if (page === 'changelog') loadChangelog();
|
||||
if (page === 'gm') {
|
||||
@@ -483,492 +475,6 @@
|
||||
saveLoadout();
|
||||
});
|
||||
|
||||
// ---------------- 支援兵 ----------------
|
||||
let cpAccounts = [], cpCharacters = [], cpState = null, cpSelected = new Set();
|
||||
let cpAIProfiles = {}, cpAISource = '';
|
||||
let cpLoading = false;
|
||||
const CP_AI_DEFAULTS = {
|
||||
behavior:'aggressive', buff_policy:'leader_first', combo_preset:'auto',
|
||||
combo_enabled:true, aoe_min_targets:3, protect_leader:true,
|
||||
tuning:{think_interval_ms:120,assist_attack_interval_ms:1800,assist_leash_x:180,assist_leash_y:90,
|
||||
chase_range:50,vision_range:10000,target_stickiness_ms:1200,heal_leader_percent:85,
|
||||
heal_self_percent:85,retry_ms:300,support_retry_ms:1500,busy_move_ms:2500,stuck_ms:700,
|
||||
detour_ms:900,give_up_ms:3500,follow_distance:75}
|
||||
};
|
||||
|
||||
async function loadCompanion() {
|
||||
if (cpLoading) return;
|
||||
cpLoading = true;
|
||||
$('cpStatus').textContent = '读取账号和角色…';
|
||||
try {
|
||||
const a = await api('/api/accounts');
|
||||
const ba = await api('/api/basic-attack');
|
||||
$('cpBasicAttack').checked = !ba || ba.enabled !== false;
|
||||
cpAccounts = (a && a.data) || [];
|
||||
const sel = $('cpAccount');
|
||||
sel.innerHTML = cpAccounts.map(x => `<option value="${x.account_id || x.username || ''}">${x.username || x.account_id}</option>`).join('');
|
||||
if (!cpAccounts.length) { $('cpStatus').textContent = '没有账号'; return; }
|
||||
await loadCompanionCharacters();
|
||||
$('cpStatus').textContent = '';
|
||||
} catch (e) {
|
||||
$('cpStatus').textContent = '读取失败:' + e.message;
|
||||
} finally { cpLoading = false; }
|
||||
}
|
||||
|
||||
async function loadCompanionCharacters() {
|
||||
const account = $('cpAccount').value;
|
||||
const r = await api('/api/characters?account=' + encodeURIComponent(account));
|
||||
cpCharacters = (r && r.data) || [];
|
||||
const owner = $('cpOwner');
|
||||
owner.innerHTML = cpCharacters.map(c => `<option value="${c.character_id}">${c.name} · Lv${c.level}</option>`).join('');
|
||||
if (cpCharacters.length) await loadCompanionState();
|
||||
else { $('cpGrid').innerHTML = '<div class="empty-box">当前账号没有角色</div>'; }
|
||||
}
|
||||
|
||||
async function loadCompanionState() {
|
||||
const owner = $('cpOwner').value;
|
||||
if (!owner) return;
|
||||
$('cpStatus').textContent = '读取支援兵配置…';
|
||||
try {
|
||||
const r = await api('/api/companion?character=' + encodeURIComponent(owner));
|
||||
if (!r.ok) { $('cpStatus').textContent = r.error || '读取失败'; return; }
|
||||
cpState = r.data || {};
|
||||
const cfg = (cpState.state && cpState.state.config) || {};
|
||||
$('cpEnabled').checked = !!cfg.enabled;
|
||||
$('cpBalance').checked = !!cfg.balance_enabled;
|
||||
$('cpProgression').checked = !!cfg.progression_enabled;
|
||||
$('cpRandom').checked = !!cfg.random_equipment_enabled;
|
||||
cpSelected = new Set((cfg.source_character_ids || []).map(String));
|
||||
cpAIProfiles = JSON.parse(JSON.stringify(cfg.ai_profiles || {}));
|
||||
cpAISource = [...cpSelected][0] || '';
|
||||
renderCompanionGrid();
|
||||
$('cpStatus').textContent = (cpState.state && cpState.state.preflight && cpState.state.preflight.reason_message) || '已读取';
|
||||
} catch (e) { $('cpStatus').textContent = '读取失败:' + e.message; }
|
||||
}
|
||||
|
||||
function renderCompanionGrid() {
|
||||
const owner = String($('cpOwner').value || '');
|
||||
const list = cpCharacters.filter(c => String(c.character_id) !== owner);
|
||||
const grid = $('cpGrid');
|
||||
if (!list.length) { grid.innerHTML = '<div class="empty-box">当前账号没有其他角色可作为支援兵</div>'; renderCpAI(); return; }
|
||||
grid.innerHTML = list.map(c => {
|
||||
const id = String(c.character_id), selected = cpSelected.has(id);
|
||||
return `<label class="cp-card ${selected?'selected':''}">
|
||||
<input type="checkbox" data-cp-source="${id}" ${selected?'checked':''}>
|
||||
<b>${c.name || id}</b><small>Lv${c.level ?? '-'} · 职业 ${c.job ?? '-'}</small>
|
||||
</label>`;
|
||||
}).join('');
|
||||
grid.querySelectorAll('input[data-cp-source]').forEach(inp => {
|
||||
inp.onchange = () => {
|
||||
const id = inp.dataset.cpSource;
|
||||
if (inp.checked) cpSelected.add(id); else cpSelected.delete(id);
|
||||
inp.closest('.cp-card').classList.toggle('selected', inp.checked);
|
||||
renderCpAI();
|
||||
};
|
||||
});
|
||||
renderCpAI();
|
||||
}
|
||||
|
||||
function cpProfile(id) {
|
||||
const raw = cpAIProfiles[String(id)] || {};
|
||||
return {
|
||||
...CP_AI_DEFAULTS, ...raw,
|
||||
tuning: {...CP_AI_DEFAULTS.tuning, ...(raw.tuning || {})}
|
||||
};
|
||||
}
|
||||
|
||||
function renderCpAI() {
|
||||
const select = $('cpAISource');
|
||||
const ids = [...cpSelected];
|
||||
if (!ids.includes(cpAISource)) cpAISource = ids[0] || '';
|
||||
select.innerHTML = ids.map(id => {
|
||||
const c = cpCharacters.find(x => String(x.character_id) === id);
|
||||
return `<option value="${id}">${c ? c.name : id}</option>`;
|
||||
}).join('');
|
||||
if (cpAISource) select.value = cpAISource;
|
||||
if (!cpAISource) {
|
||||
$('cpAIBehavior').value = 'assist'; $('cpAIBuff').value = 'leader_first';
|
||||
$('cpAIComboPreset').value = 'auto'; $('cpAIAoe').value = 3;
|
||||
$('cpAIComboEnabled').checked = true; $('cpAIProtect').checked = true;
|
||||
document.querySelectorAll('[data-cp-tuning]').forEach(inp => {
|
||||
inp.value = CP_AI_DEFAULTS.tuning[inp.dataset.cpTuning] ?? 0;
|
||||
});
|
||||
return;
|
||||
}
|
||||
const p = cpProfile(cpAISource);
|
||||
$('cpAIBehavior').value = ['assist','aggressive'].includes(p.behavior) ? p.behavior : 'aggressive';
|
||||
$('cpAIBuff').value = p.buff_policy || 'leader_first';
|
||||
$('cpAIComboPreset').value = p.combo_preset || 'auto';
|
||||
$('cpAIAoe').value = Number(p.aoe_min_targets) || 3;
|
||||
$('cpAIComboEnabled').checked = !!p.combo_enabled;
|
||||
$('cpAIProtect').checked = !!p.protect_leader;
|
||||
document.querySelectorAll('[data-cp-tuning]').forEach(inp => {
|
||||
inp.value = p.tuning[inp.dataset.cpTuning] ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
function readCpAI() {
|
||||
if (!cpAISource) return;
|
||||
const p = cpProfile(cpAISource);
|
||||
p.behavior = $('cpAIBehavior').value;
|
||||
p.buff_policy = $('cpAIBuff').value;
|
||||
p.combo_preset = $('cpAIComboPreset').value;
|
||||
p.combo_enabled = $('cpAIComboEnabled').checked;
|
||||
p.aoe_min_targets = Math.max(2, Math.min(6, Number($('cpAIAoe').value) || 3));
|
||||
p.protect_leader = $('cpAIProtect').checked;
|
||||
p.tuning = {...p.tuning};
|
||||
document.querySelectorAll('[data-cp-tuning]').forEach(inp => {
|
||||
const n = Number(inp.value);
|
||||
if (Number.isFinite(n)) p.tuning[inp.dataset.cpTuning] = n;
|
||||
});
|
||||
cpAIProfiles[String(cpAISource)] = p;
|
||||
}
|
||||
|
||||
|
||||
$('cpAccount').addEventListener('change', async () => {
|
||||
try { await loadCompanionCharacters(); } catch (e) { $('cpStatus').textContent = e.message; }
|
||||
});
|
||||
$('cpOwner').addEventListener('change', loadCompanionState);
|
||||
$('cpReload').addEventListener('click', loadCompanion);
|
||||
$('cpAISource').addEventListener('change', () => { cpAISource = $('cpAISource').value; renderCpAI(); });
|
||||
['cpAIBehavior','cpAIBuff','cpAIComboPreset','cpAIAoe','cpAIComboEnabled','cpAIProtect'].forEach(id => {
|
||||
$(id).addEventListener('change', readCpAI);
|
||||
});
|
||||
document.querySelectorAll('[data-cp-tuning]').forEach(inp => inp.addEventListener('change', readCpAI));
|
||||
$('cpBasicAttack').addEventListener('change', async () => {
|
||||
try {
|
||||
const r = await post('/api/basic-attack', {enabled: $('cpBasicAttack').checked});
|
||||
if (!r.ok) $('cpStatus').textContent = r.error || '普攻开关保存失败';
|
||||
else $('cpStatus').textContent = $('cpBasicAttack').checked
|
||||
? '已允许支援兵普攻;重启客户端后生效。'
|
||||
: '已关闭支援兵普攻,技能释放不受影响;重启客户端后生效。';
|
||||
} catch (e) { $('cpStatus').textContent = '普攻开关保存失败:' + e.message; }
|
||||
});
|
||||
|
||||
$('cpSave').addEventListener('click', async () => {
|
||||
const owner = $('cpOwner').value;
|
||||
if (!owner) { $('cpStatus').textContent = '请先选择出战角色'; return; }
|
||||
if ($('cpEnabled').checked && cpSelected.size === 0) {
|
||||
$('cpStatus').textContent = '启用支援兵时至少选择一个支援兵角色'; return;
|
||||
}
|
||||
const state = (cpState && cpState.state) || {};
|
||||
const cfg = state.config || {};
|
||||
const sources = [...cpSelected];
|
||||
readCpAI();
|
||||
const payload = {
|
||||
characterId: String(owner),
|
||||
expectedRevision: Number(cfg.revision || 0),
|
||||
enabled: $('cpEnabled').checked,
|
||||
balanceEnabled: $('cpBalance').checked,
|
||||
progressionEnabled: $('cpProgression').checked,
|
||||
randomEquipmentEnabled: $('cpRandom').checked,
|
||||
sourceCharacterId: sources[0] || '',
|
||||
sourceCharacterIds: sources,
|
||||
ai: cfg.ai || {},
|
||||
aiProfiles: cpAIProfiles
|
||||
};
|
||||
$('cpStatus').textContent = '保存中…';
|
||||
try {
|
||||
const r = await post('/api/companion', payload);
|
||||
if (!r.ok) { $('cpStatus').textContent = r.error || '保存失败'; return; }
|
||||
cpState = r.data || cpState;
|
||||
$('cpStatus').textContent = '支援兵配置已保存,下一次进入副本生效。';
|
||||
} catch (e) { $('cpStatus').textContent = '保存失败:' + e.message; }
|
||||
});
|
||||
|
||||
// ---------------- 服务端工具 ----------------
|
||||
async function loadServerTool() {
|
||||
try {
|
||||
$('stBody').innerHTML = '<div class="empty-box">正在读取服务端配置…</div>';
|
||||
const [f, a, s, g, b] = await Promise.all([api('/api/servertool/features'), api('/api/servertool/activity'), api('/api/status'), api('/api/servertool/gameplay'), api('/api/servertool/batch-pickup')]);
|
||||
stFeatures = f.data || {}; stActivity = a.data || {}; stGameplay = g.data || {};
|
||||
stBatchPickup = !!(b.data || {}).enabled;
|
||||
window.__stStatusInfo = s || {};
|
||||
$('stSource').textContent = f.source === 'native' ? 'Native API' : '文件配置';
|
||||
renderServerTool();
|
||||
} catch (e) {
|
||||
$('stBody').innerHTML = '<div class="empty-box">读取失败:' + (e && e.message ? e.message : e) + '</div>';
|
||||
$('stStatus').textContent = '读取失败';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.st-tab').forEach(b => b.addEventListener('click', () => {
|
||||
stTab = b.dataset.st;
|
||||
document.querySelectorAll('.st-tab').forEach(x => x.classList.toggle('active', x === b));
|
||||
renderServerTool();
|
||||
}));
|
||||
|
||||
const LABEL = {
|
||||
raidReviveSeconds:'副本复活秒数', allDeadReturnSeconds:'全员死亡返回秒数', allowFreeRevive:'允许免费复活',
|
||||
hellOrbChance:'深渊宝珠概率', hellDropChanceDenominator:'掉落分母', equipmentRollMultiplier:'装备倍率',
|
||||
epicPieceRollMultiplier:'史诗碎片倍率', lukeEnergyRecoveryPercent:'卢克能量恢复%', fiendWarSquadCount:'超时空队伍数',
|
||||
roomFatigueCost:'每房间疲劳', dailyFatigueResetHour:'每日疲劳重置点', adventureGroupBonusPercent:'冒险团加成%',
|
||||
rentalMaxReinforce:'租赁最大强化', rentalMaxForge:'租赁最大锻造', soloTeleportCooldownSeconds:'单人传送冷却秒',
|
||||
soloTeleportCooldownIndices:'单人传送索引', auctionStandardListingLimit:'普通拍卖上架数',
|
||||
auctionPremiumListingLimit:'高级拍卖上架数', auctionStandardDurationHours:'普通拍卖时长',
|
||||
auctionPremiumDurationHours:'高级拍卖时长', auctionStandardFeePercent:'普通手续费%', auctionPremiumFeePercent:'高级手续费%',
|
||||
antonBlackFogRecoverySeconds:'安徒恩黑雾恢复秒', antonBlackFogActiveSeconds:'安徒恩黑雾持续秒',
|
||||
antonQuakeRecoverySeconds:'安徒恩震颤恢复秒', antonQuakeActiveSeconds:'安徒恩震颤持续秒',
|
||||
antonNavalOpenDelaySeconds:'安徒恩舰炮延迟秒', antonNavalRecoverySeconds:'安徒恩舰炮恢复秒',
|
||||
antonNavalActiveSeconds:'安徒恩舰炮持续秒', lukeEnergyRecovery0Seconds:'卢克1阶段恢复秒',
|
||||
lukeEnergyRecovery1Seconds:'卢克2阶段恢复秒', lukeEnergyRecovery2Seconds:'卢克3阶段恢复秒',
|
||||
lukePhaseTimeoutPercent:'卢克阶段时限%', fiendWarAreaLimitPercent:'超时空区域限制%',
|
||||
fiendWarDieImprisonPercent:'超时空死亡禁锢%', antonMemberMax:'安徒恩人数上限',
|
||||
antonStartMinimumMembers:'安徒恩最少开局人数', antonSingleStartMinimumMembers:'安徒恩单人开局人数',
|
||||
antonStartDelaySeconds:'安徒恩开始延迟', antonPhaseBreakSeconds:'安徒恩阶段间隔',
|
||||
antonEntryCostItem:'安徒恩入场物品', antonEntryCostCount:'安徒恩入场数量',
|
||||
antonWeeklyEnterCountA:'安徒恩A每周次数', antonWeeklyEnterCountB:'安徒恩B每周次数',
|
||||
antonPhaseTimeoutSeconds:'安徒恩阶段时限', lukeDailyAccountEnterLimit:'卢克账号每日次数',
|
||||
lukeSanctumTimerCount:'卢克圣所计时', lukeEntryCostItem:'卢克入场物品', lukeEntryCostCount:'卢克入场数量',
|
||||
fiendWarDailyEnterLimit:'超时空每日次数', fiendWarMemberMax:'超时空人数上限',
|
||||
fiendWarStartMinimumMembers:'超时空最少开局人数', fiendWarDisconnectWaitSeconds:'超时空掉线等待',
|
||||
fiendWarEntryCostItem:'超时空入场物品', fiendWarEntryCostCount:'超时空入场数量', fiendWarRetry:'超时空重试',
|
||||
reviveByDungeon:'按副本复活配置'
|
||||
};
|
||||
|
||||
const ST_DESC = {
|
||||
soloTeleportCooldownSeconds:'单人传送冷却时间,单位秒;服务端读取该值限制重复传送。',
|
||||
soloTeleportCooldownIndices:'允许触发单人传送的技能索引,多个用逗号分隔。',
|
||||
auctionStandardListingLimit:'普通拍卖行同时可上架数量。',
|
||||
auctionStandardDurationHours:'普通拍卖行上架时长,单位小时。',
|
||||
auctionPremiumListingLimit:'高级拍卖行同时可上架数量。',
|
||||
auctionPremiumDurationHours:'高级拍卖行上架时长,单位小时。',
|
||||
auctionStandardFeePercent:'普通拍卖成交手续费百分比。',
|
||||
auctionPremiumFeePercent:'高级拍卖成交手续费百分比。',
|
||||
raidReviveSeconds:'团本死亡后等待复活的秒数,有效范围 1-60;不是无限复活选项。',
|
||||
allDeadReturnSeconds:'队伍全员死亡后返回城镇的等待秒数。',
|
||||
hellOrbChance:'深渊宝珠掉落判定值。',
|
||||
hellDropChanceDenominator:'深渊基础掉率分母,数值越大掉率越低。',
|
||||
equipmentRollMultiplier:'史诗装备掉落 roll 次数倍率。',
|
||||
epicPieceRollMultiplier:'史诗碎片掉落 roll 次数倍率。',
|
||||
allowFreeRevive:'允许不使用复活币直接复活。',
|
||||
roomFatigueCost:'进入每个房间消耗的疲劳值。',
|
||||
dailyFatigueResetHour:'每日疲劳重置小时,0-23。',
|
||||
adventureGroupBonusPercent:'冒险团经验加成百分比。',
|
||||
rentalMaxReinforce:'租赁装备允许的最大强化等级。',
|
||||
rentalMaxForge:'租赁装备允许的最大锻造等级。',
|
||||
antonBlackFogRecoverySeconds:'安徒恩黑雾之源恢复等待秒数。',
|
||||
antonBlackFogActiveSeconds:'安徒恩黑雾之源持续时间秒数。',
|
||||
antonQuakeRecoverySeconds:'安徒恩震颤之地恢复等待秒数。',
|
||||
antonQuakeActiveSeconds:'安徒恩震颤之地持续时间秒数。',
|
||||
antonNavalOpenDelaySeconds:'安徒恩舰炮防御战开启延迟秒数。',
|
||||
antonNavalRecoverySeconds:'安徒恩舰炮防御战恢复等待秒数。',
|
||||
antonNavalActiveSeconds:'安徒恩舰炮防御战持续时间秒数。',
|
||||
lukeEnergyRecovery0Seconds:'卢克能量控制一阶段恢复秒数。',
|
||||
lukeEnergyRecovery1Seconds:'卢克能量控制二阶段恢复秒数。',
|
||||
lukeEnergyRecovery2Seconds:'卢克能量控制三阶段恢复秒数。',
|
||||
lukePhaseTimeoutPercent:'卢克各阶段时限百分比。',
|
||||
lukeEnergyRecoveryPercent:'卢克能量恢复速度百分比。',
|
||||
fiendWarSquadCount:'超时空之战小队编制数量。',
|
||||
fiendWarAreaLimitPercent:'超时空区域进入限制百分比。',
|
||||
fiendWarDieImprisonPercent:'超时空死亡禁锢触发百分比。'
|
||||
};
|
||||
|
||||
const ST_FIELDS = {
|
||||
global: [
|
||||
'soloTeleportCooldownSeconds','soloTeleportCooldownIndices',
|
||||
'auctionStandardListingLimit','auctionStandardDurationHours',
|
||||
'auctionPremiumListingLimit','auctionPremiumDurationHours',
|
||||
'auctionStandardFeePercent','auctionPremiumFeePercent'
|
||||
],
|
||||
gameplay: [
|
||||
'allDeadReturnSeconds','hellOrbChance',
|
||||
'hellDropChanceDenominator','equipmentRollMultiplier',
|
||||
'epicPieceRollMultiplier','allowFreeRevive'
|
||||
],
|
||||
growth: [
|
||||
'roomFatigueCost','dailyFatigueResetHour','adventureGroupBonusPercent',
|
||||
'rentalMaxReinforce','rentalMaxForge'
|
||||
],
|
||||
raid: {
|
||||
anton: [
|
||||
'antonBlackFogRecoverySeconds','antonBlackFogActiveSeconds',
|
||||
'antonQuakeRecoverySeconds','antonQuakeActiveSeconds',
|
||||
'antonNavalOpenDelaySeconds','antonNavalRecoverySeconds','antonNavalActiveSeconds'
|
||||
],
|
||||
luke: [
|
||||
'lukeEnergyRecovery0Seconds','lukeEnergyRecovery1Seconds',
|
||||
'lukeEnergyRecovery2Seconds','lukePhaseTimeoutPercent',
|
||||
'lukeEnergyRecoveryPercent'
|
||||
],
|
||||
fiend: [
|
||||
'fiendWarSquadCount','fiendWarAreaLimitPercent','fiendWarDieImprisonPercent'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function allAssignedKeys() {
|
||||
return new Set([...ST_FIELDS.global, ...ST_FIELDS.gameplay, ...ST_FIELDS.growth,
|
||||
...ST_FIELDS.raid.anton, ...ST_FIELDS.raid.luke, ...ST_FIELDS.raid.fiend]);
|
||||
}
|
||||
|
||||
function renderServerTool() {
|
||||
const body = $('stBody');
|
||||
if (stTab === 'activity') { renderActivity(body); return; }
|
||||
if (stTab === 'launch') {
|
||||
body.innerHTML = `<div class="st-section"><h3>启动控制</h3><p class="st-note">配置文件:server-features.json / activity.json。服务端运行状态请以开始页为准。</p><div class="lo-toolbar"><button class="shell-btn primary" id="stStart">开始游戏</button><button class="shell-btn" id="stStop">关闭服务端</button></div><div class="st-note" id="stLaunchInfo"></div></div>`;
|
||||
$('stStart').onclick = () => doAction('start');
|
||||
$('stStop').onclick = () => doAction('stop');
|
||||
api('/api/status').then(s => {
|
||||
$('stLaunchInfo').textContent = `服务端:${s.server_running ? '运行中' : '未运行'} · 目录:${s.server_root || '未绑定'}`;
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
let sub = '';
|
||||
let keys;
|
||||
if (stTab === 'features') {
|
||||
keys = Object.keys(stFeatures).filter(k => !allAssignedKeys().has(k));
|
||||
} else if (stTab === 'raid') {
|
||||
const names = {anton:'安徒恩机制', luke:'卢克', fiend:'超时空'};
|
||||
$('stSubTabs').innerHTML = Object.keys(names).map(k =>
|
||||
`<button class="st-subtab ${stRaidTab===k?'active':''}" data-raid="${k}">${names[k]}</button>`).join('');
|
||||
$('stSubTabs').querySelectorAll('.st-subtab').forEach(b => b.onclick = () => {
|
||||
stRaidTab = b.dataset.raid; renderServerTool();
|
||||
});
|
||||
keys = ST_FIELDS.raid[stRaidTab] || [];
|
||||
} else {
|
||||
$('stSubTabs').innerHTML = '';
|
||||
keys = ST_FIELDS[stTab] || [];
|
||||
}
|
||||
let html = `<div class="st-section"><h3>${tabTitle(stTab)}</h3>`;
|
||||
if (stTab === 'features') {
|
||||
const info = window.__stStatusInfo || {};
|
||||
const rawNames = ['instance.json','dnfbridge.toml','logic.toml','plan.json'];
|
||||
$('stSubTabs').innerHTML = rawNames.map(n =>
|
||||
`<button class="st-subtab ${stFeatureTab===n?'active':''}" data-raw="${n}">${n}</button>`).join('');
|
||||
$('stSubTabs').querySelectorAll('.st-subtab').forEach(b => b.onclick = () => {
|
||||
stFeatureTab = b.dataset.raw; renderServerTool();
|
||||
});
|
||||
html += `<div class="st-form">
|
||||
<label>服务端目录<input value="${(info.server_root || '').replace(/"/g,'"')}" readonly></label>
|
||||
<label>Admin 端口<input value="18111" readonly></label>
|
||||
<label>数据库<input value="${(info.db || '').replace(/"/g,'"')}" readonly></label>
|
||||
<label>客户端目录<input value="${(info.client_dir || '').replace(/"/g,'"')}" readonly></label>
|
||||
</div>`;
|
||||
html += `<div class="st-note">当前配置文件:${stFeatureTab}</div>`;
|
||||
html += `<textarea id="stRaw" class="st-raw" spellcheck="false">${stRawText.replace(/</g,'<')}</textarea>`;
|
||||
html += `<div class="st-note">服务端参数页保留实例与配置文件;下方列出未归入其他页的字段。</div>`;
|
||||
}
|
||||
html += `<div class="st-form">${groupFields(stFeatures, stTab === 'gameplay' ? keys.filter(k => k !== 'allowFreeRevive') : keys)}</div>`;
|
||||
if (stTab === 'gameplay') {
|
||||
html += `<div class="st-form">
|
||||
<label>深渊史诗掉率(%)<small class="st-hint">100 = 原版,250 = 2.5 倍</small><input data-game-key="hellEpicDropRatePercent" type="number" value="${stGameplay.hell_epic_drop_rate_percent ?? 0}"></label>
|
||||
<label>史诗药剂测试倍率(%)<small class="st-hint">0 = 关闭测试倍率</small><input data-game-key="epicPotionMultiplierPercent" type="number" value="${stGameplay.epic_drop_rate_potion_test_multiplier_percent ?? 0}"></label>
|
||||
</div>`;
|
||||
html += `<div class="st-check-row">
|
||||
<label class="st-check"><input type="checkbox" data-st-key="allowFreeRevive" ${stFeatures.allowFreeRevive === true ? 'checked' : ''}>允许无复活币免费复活</label>
|
||||
<label class="st-check"><input type="checkbox" data-batch-pickup ${stBatchPickup ? 'checked' : ''}>服务端允许一键拾取</label>
|
||||
</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
html += `<details class="st-json"><summary>高级:完整 JSON 配置</summary><textarea id="stJson" spellcheck="false"></textarea><div class="lo-toolbar"><button class="shell-btn" id="stJsonApply">应用 JSON</button><span class="muted">可直接编辑全部服务端字段</span></div></details>`;
|
||||
body.innerHTML = html;
|
||||
if (stTab === 'features') loadRawConfig(stFeatureTab);
|
||||
const jsonBox = $('stJson');
|
||||
if (jsonBox) jsonBox.value = JSON.stringify(stFeatures, null, 2);
|
||||
$('stJsonApply').onclick = () => {
|
||||
try {
|
||||
const obj = JSON.parse($('stJson').value);
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw new Error('需要 JSON 对象');
|
||||
stFeatures = obj; renderServerTool();
|
||||
$('stStatus').textContent = 'JSON 已载入,点击“保存当前页”写入';
|
||||
} catch (e) { $('stStatus').textContent = 'JSON 错误:' + e.message; }
|
||||
};
|
||||
bindFeatureInputs(body);
|
||||
}
|
||||
|
||||
async function loadRawConfig(name) {
|
||||
try {
|
||||
const r = await api('/api/servertool/raw?name=' + encodeURIComponent(name));
|
||||
stRawText = r.text || '';
|
||||
const box = $('stRaw');
|
||||
if (box) box.value = stRawText;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function bindFeatureInputs(body) {
|
||||
body.querySelectorAll('input[data-st-key]').forEach(inp => {
|
||||
inp.addEventListener('change', () => {
|
||||
const key = inp.dataset.stKey;
|
||||
let v = inp.value;
|
||||
if (inp.type === 'checkbox') v = inp.checked;
|
||||
else if (inp.dataset.type === 'number') { const n = Number(v); v = Number.isFinite(n) ? n : 0; }
|
||||
stFeatures[key] = v;
|
||||
});
|
||||
});
|
||||
body.querySelectorAll('input[data-game-key]').forEach(inp => {
|
||||
inp.addEventListener('change', () => {
|
||||
stGameplay[inp.dataset.gameKey] = Number(inp.value) || 0;
|
||||
});
|
||||
});
|
||||
body.querySelectorAll('input[data-batch-pickup]').forEach(inp => {
|
||||
inp.addEventListener('change', () => { stBatchPickup = inp.checked; });
|
||||
});
|
||||
}
|
||||
|
||||
function groupFields(obj, keys) {
|
||||
return keys.map(k => {
|
||||
const val = obj[k];
|
||||
const label = LABEL[k] || k;
|
||||
if (typeof val === 'boolean' || /Retry|Revive|Pickup/.test(k)) {
|
||||
return `<label class="st-check"><input type="checkbox" data-st-key="${k}" ${val===true?'checked':''}>${label}</label>`;
|
||||
}
|
||||
const numeric = (typeof val === 'number') || /Seconds|Percent|Count|Hour|Limit|Cost|Max|Min|Delay|Item/.test(k);
|
||||
const type = numeric ? 'number' : 'text';
|
||||
const shown = val === undefined || val === null ? '' : String(val);
|
||||
const hint = ST_DESC[k] ? `<small class="st-hint">${ST_DESC[k]}</small>` : '';
|
||||
return `<label>${label}${hint}<input data-st-key="${k}" data-type="${type}" type="${type}" value="${shown.replace(/"/g,'"')}"></label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function tabTitle(tab) {
|
||||
return ({features:'服务端参数',global:'全局游戏参数',gameplay:'游戏参数',growth:'疲劳/成长',raid:'团本配置'})[tab] || tab;
|
||||
}
|
||||
function tabKeys(tab) {
|
||||
const all = Object.keys(stFeatures);
|
||||
const match = {
|
||||
features: ['raidReviveSeconds','allDeadReturnSeconds','allowFreeRevive','hellOrbChance','hellDropChanceDenominator','equipmentRollMultiplier','epicPieceRollMultiplier','lukeEnergyRecoveryPercent','fiendWarSquadCount'],
|
||||
global: [],
|
||||
gameplay: ['hellOrbChance','hellDropChanceDenominator','equipmentRollMultiplier','epicPieceRollMultiplier','roomFatigueCost','rentalMaxReinforce','rentalMaxForge'],
|
||||
growth: ['roomFatigueCost','dailyFatigueResetHour','adventureGroupBonusPercent','rentalMaxReinforce','rentalMaxForge'],
|
||||
raid: ['antonBlackFogRecoverySeconds','antonBlackFogActiveSeconds','antonQuakeRecoverySeconds','antonQuakeActiveSeconds','antonNavalOpenDelaySeconds','antonNavalRecoverySeconds','antonNavalActiveSeconds','lukeEnergyRecovery0Seconds','lukeEnergyRecovery1Seconds','lukeEnergyRecovery2Seconds','lukePhaseTimeoutPercent','fiendWarAreaLimitPercent','fiendWarDieImprisonPercent','antonMemberMax','antonStartMinimumMembers','lukeDailyAccountEnterLimit','fiendWarDailyEnterLimit'],
|
||||
};
|
||||
if (tab === 'global') return all.filter(k => /soloTeleport|auction|allDead|revive/i.test(k));
|
||||
return (match[tab] || []).filter(k => all.includes(k));
|
||||
}
|
||||
|
||||
function renderActivity(body) {
|
||||
const keys = Object.keys(stActivity);
|
||||
body.innerHTML = `<div class="st-section"><h3>在线活动开关</h3><div class="st-form">${keys.map(k => {
|
||||
const v = !!stActivity[k]; const label = ({daily:'每日签到',weekly:'每周签到',monthly:'每月签到',marble:'签到弹珠',returnee:'回归活动',returneeGift:'回归赠礼',lucky:'幸运活动',levelAiming:'升级预约',laborDay:'劳动节',legacyLevelup:'旧版升级',pcRoom:'网吧时间点',spendTime:'在线时长'})[k] || k;
|
||||
return `<label class="st-check"><input type="checkbox" data-act-key="${k}" ${v?'checked':''}>${label}</label>`;
|
||||
}).join('')}</div></div>`;
|
||||
body.querySelectorAll('input[data-act-key]').forEach(inp => {
|
||||
inp.addEventListener('change', () => { stActivity[inp.dataset.actKey] = inp.checked; });
|
||||
});
|
||||
}
|
||||
|
||||
$('stReload').addEventListener('click', loadServerTool);
|
||||
$('stSave').addEventListener('click', async () => {
|
||||
try {
|
||||
let r;
|
||||
if (stTab === 'activity') {
|
||||
r = await post('/api/servertool/activity', stActivity);
|
||||
} else if (stTab === 'features' && $('stRaw')) {
|
||||
r = await post('/api/servertool/raw', {name: stFeatureTab, text: $('stRaw').value});
|
||||
} else if (stTab === 'gameplay') {
|
||||
r = await post('/api/servertool/features', stFeatures);
|
||||
const g = await post('/api/servertool/gameplay', stGameplay);
|
||||
const bp = await post('/api/servertool/batch-pickup', {enabled: stBatchPickup});
|
||||
if (!g.ok) r = g;
|
||||
if (!bp.ok) r = bp;
|
||||
} else {
|
||||
r = await post('/api/servertool/features', stFeatures);
|
||||
}
|
||||
$('stStatus').textContent = r.ok ? ('已保存' + (r.warning ? '(' + r.warning + ')' : '')) : (r.error || '保存失败');
|
||||
} catch (e) { $('stStatus').textContent = '保存失败:' + e.message; }
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const s = await api('/api/status');
|
||||
|
||||
Reference in new issue
Block a user