admin update

master
Denis Ranneft 1 month ago
parent 11d2c41e90
commit 9b5af1f93c

@ -85,9 +85,9 @@
contentQuests: [],
contentGearRows: [],
contentGearEditor: null,
contentQuestEditor: null,
contentEnemies: [],
contentEnemyEditor: null,
contentQuestEditor: null,
gearFilterSlot: "",
gearFilterRarity: "",
gearFilterSubtype: "",
@ -117,11 +117,8 @@
/** Matches model.AllBuffTypes / AllDebuffTypes (admin manual apply). */
const ADMIN_BUFF_TYPES = ["rush", "rage", "shield", "luck", "resurrection", "heal", "power_potion", "war_cry"];
const ADMIN_DEBUFF_TYPES = ["poison", "freeze", "burn", "stun", "slow", "weaken", "ice_slow"];
/** model.SpecialAbility — чекбоксы в редакторе врагов */
const ADMIN_ENEMY_SPECIAL_ABILITIES = [
"burn", "slow", "critical", "poison", "freeze", "ice_slow", "stun", "dodge",
"regen", "burst", "chain_lightning", "summon"
];
/** model.SpecialAbility — enemy template abilities (DB text[]). */
const ADMIN_ENEMY_ABILITIES = ["burn", "slow", "critical", "poison", "freeze", "ice_slow", "stun", "dodge", "regen", "burst", "chain_lightning", "summon"];
function e(v) { return String(v ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"); }
function authHeader() { return `Basic ${btoa(`${state.auth.username}:${state.auth.password}`)}`; }
@ -968,93 +965,53 @@
render();
}
async function loadContentGearBase() { const data = await api("content/gear-base"); state.contentGearRows = data.gear || []; render(); }
async function loadContentQuests() { const data = await api("content/quests"); state.contentQuests = data.quests || []; render(); }
async function loadContentEnemies() {
const data = await api("content/enemies");
state.contentEnemies = data.enemies || [];
render();
}
function openContentEnemyEditorByType(enemyType) {
const row = (state.contentEnemies || []).find(x => x.type === enemyType);
async function reloadEnemyTemplatesOnly() {
await api("content/enemies/reload", { method: "POST", body: "{}" });
setMessage("Enemy templates reloaded from DB into server memory");
}
function openContentEnemyEditorByType(type) {
const row = (state.contentEnemies || []).find(x => x.type === type);
if (!row) return;
state.contentEnemyEditor = Object.assign({}, row, {
_abilities: new Set(row.specialAbilities || [])
_abilitiesText: Array.isArray(row.specialAbilities) ? row.specialAbilities.join(", ") : ""
});
render();
}
function closeContentEnemyEditor() { state.contentEnemyEditor = null; render(); }
async function saveContentEnemy() {
const ed = state.contentEnemyEditor;
if (!ed) return;
const type = String(ed.type || "").trim();
const maxHp = Number(document.getElementById("cem-maxHp").value || 0);
const typ = document.getElementById("me-type")?.value?.trim();
if (!typ) return;
const maxHp = Number(document.getElementById("me-maxHp").value || 0);
const abText = document.getElementById("me-abilities").value || "";
const specialAbilities = abText.split(",").map(s => s.trim()).filter(Boolean);
const body = {
type,
name: document.getElementById("cem-name").value.trim(),
maxHp,
id: Number(document.getElementById("me-id").value || 0),
type: typ,
name: document.getElementById("me-name").value,
hp: maxHp,
attack: Number(document.getElementById("cem-attack").value || 0),
defense: Number(document.getElementById("cem-defense").value || 0),
speed: Number(document.getElementById("cem-speed").value || 0),
critChance: Number(document.getElementById("cem-critChance").value || 0),
minLevel: Number(document.getElementById("cem-minLevel").value || 1),
maxLevel: Number(document.getElementById("cem-maxLevel").value || 1),
xpReward: Number(document.getElementById("cem-xpReward").value || 0),
goldReward: Number(document.getElementById("cem-goldReward").value || 0),
isElite: !!document.getElementById("cem-isElite").checked,
specialAbilities: ADMIN_ENEMY_SPECIAL_ABILITIES.filter(a => document.getElementById("cem-ab-" + a).checked)
maxHp,
attack: Number(document.getElementById("me-attack").value || 0),
defense: Number(document.getElementById("me-defense").value || 0),
speed: Number(document.getElementById("me-speed").value || 0),
critChance: Number(document.getElementById("me-critChance").value || 0),
minLevel: Number(document.getElementById("me-minLevel").value || 1),
maxLevel: Number(document.getElementById("me-maxLevel").value || 100),
xpReward: Number(document.getElementById("me-xpReward").value || 0),
goldReward: Number(document.getElementById("me-goldReward").value || 0),
specialAbilities,
isElite: !!document.getElementById("me-isElite")?.checked
};
await api("content/enemies/" + encodeURIComponent(type), { method: "PUT", body: JSON.stringify(body) });
await api(`content/enemies/${encodeURIComponent(typ)}`, { method: "PUT", body: JSON.stringify(body) });
state.contentEnemyEditor = null;
await loadContentEnemies();
setMessage("Enemy template saved; in-memory templates reloaded");
}
async function reloadEnemyTemplatesOnly() {
await api("content/enemies/reload", { method: "POST", body: "{}" });
setMessage("Enemy templates reloaded from DB");
}
function contentEnemyEditorHtml() {
const ed = state.contentEnemyEditor;
if (!ed) return "";
const abs = ed._abilities instanceof Set ? ed._abilities : new Set(ed.specialAbilities || []);
const abChecks = ADMIN_ENEMY_SPECIAL_ABILITIES.map(a =>
`<label style="display:inline-block;margin:4px 10px 4px 0"><input type="checkbox" id="cem-ab-${a}" ${abs.has(a) ? "checked" : ""} /> ${e(a)}</label>`
).join("");
return `
<div class="card">
<h4>Edit enemy: <kbd>${e(ed.type)}</kbd></h4>
<p class="muted">Запись в таблице <kbd>enemies</kbd>. После сохранения сервер подставляет <kbd>hp</kbd> = <kbd>maxHp</kbd> и перезагружает шаблоны в памяти.</p>
<input type="hidden" id="cem-type" value="${e(ed.type)}" />
<div class="row-2">
<div><label>type (id строки)</label><input id="cem-type-ro" value="${e(ed.type)}" disabled /></div>
<div><label>name</label><input id="cem-name" value="${e(ed.name)}" /></div>
</div>
<div class="row-2">
<div><label>maxHp</label><input id="cem-maxHp" type="number" value="${e(ed.maxHp)}" title="Базовые HP шаблона" /></div>
<div><label>isElite</label><label style="display:block;margin-top:8px"><input type="checkbox" id="cem-isElite" ${ed.isElite ? "checked" : ""} /> elite</label></div>
</div>
<div class="row-2">
<div><label>attack</label><input id="cem-attack" type="number" value="${e(ed.attack)}" /></div>
<div><label>defense</label><input id="cem-defense" type="number" value="${e(ed.defense)}" /></div>
</div>
<div class="row-2">
<div><label>speed (атак/сек)</label><input id="cem-speed" type="number" step="any" value="${e(ed.speed)}" /></div>
<div><label>critChance (01)</label><input id="cem-critChance" type="number" step="any" value="${e(ed.critChance)}" /></div>
</div>
<div class="row-2">
<div><label>minLevel</label><input id="cem-minLevel" type="number" value="${e(ed.minLevel)}" /></div>
<div><label>maxLevel</label><input id="cem-maxLevel" type="number" value="${e(ed.maxLevel)}" /></div>
</div>
<div class="row-2">
<div><label>xpReward</label><input id="cem-xpReward" type="number" value="${e(ed.xpReward)}" /></div>
<div><label>goldReward</label><input id="cem-goldReward" type="number" value="${e(ed.goldReward)}" /></div>
</div>
<div><span class="muted">Special abilities</span></div>
<div style="margin:6px 0 10px">${abChecks}</div>
<button class="btn" onclick="withAction(saveContentEnemy)">Save</button>
<button class="btn" onclick="closeContentEnemyEditor()">Cancel</button>
</div>`;
setMessage("Monster template saved; in-memory templates updated");
}
async function loadContentQuests() { const data = await api("content/quests"); state.contentQuests = data.quests || []; render(); }
function openNewContentGearEditor() {
state.contentGearEditor = {
id: 0, slot: "main_hand", formId: "", name: "", subtype: "", rarity: "common", ilvl: 1,
@ -1216,6 +1173,46 @@
<button class="btn" onclick="closeContentQuestEditor()">Cancel</button>
</div>`;
}
function contentEnemyEditorHtml() {
const ed = state.contentEnemyEditor;
if (!ed) return "";
const abHint = ADMIN_ENEMY_ABILITIES.join(", ");
const abVal = ed._abilitiesText != null ? ed._abilitiesText : (Array.isArray(ed.specialAbilities) ? ed.specialAbilities.join(", ") : "");
return `
<div class="card">
<h4>Edit monster template</h4>
<p class="muted">Таблица <kbd>enemies</kbd>. Поле <kbd>type</kbd> не меняется (ключ). После сохранения шаблоны подхватываются в память сервера.</p>
<input type="hidden" id="me-id" value="${e(ed.id)}" />
<div class="row-2">
<div><label>type (read-only)</label><input id="me-type" readonly value="${e(ed.type)}" /></div>
<div><label>name</label><input id="me-name" value="${e(ed.name)}" /></div>
</div>
<div class="row-2">
<div><label>maxHp</label><input id="me-maxHp" type="number" value="${e(ed.maxHp)}" /></div>
<div><label>isElite</label><label style="display:flex;align-items:center;gap:8px;margin-top:8px"><input id="me-isElite" type="checkbox" ${ed.isElite ? "checked" : ""} /> elite</label></div>
</div>
<div class="row-2">
<div><label>attack</label><input id="me-attack" type="number" value="${e(ed.attack)}" /></div>
<div><label>defense</label><input id="me-defense" type="number" value="${e(ed.defense)}" /></div>
</div>
<div class="row-2">
<div><label>speed</label><input id="me-speed" type="number" step="any" value="${e(ed.speed)}" /></div>
<div><label>critChance (01)</label><input id="me-critChance" type="number" step="any" value="${e(ed.critChance)}" /></div>
</div>
<div class="row-2">
<div><label>minLevel</label><input id="me-minLevel" type="number" value="${e(ed.minLevel)}" /></div>
<div><label>maxLevel</label><input id="me-maxLevel" type="number" value="${e(ed.maxLevel)}" /></div>
</div>
<div class="row-2">
<div><label>xpReward</label><input id="me-xpReward" type="number" value="${e(ed.xpReward)}" /></div>
<div><label>goldReward</label><input id="me-goldReward" type="number" value="${e(ed.goldReward)}" /></div>
</div>
<div><label>specialAbilities (через запятую)</label><input id="me-abilities" value="${e(abVal)}" placeholder="burn, regen" /></div>
<p class="muted" style="margin-top:4px">Допустимые теги: ${e(abHint)}</p>
<button class="btn" onclick="withAction(saveContentEnemy)">Сохранить</button>
<button class="btn" onclick="closeContentEnemyEditor()">Отмена</button>
</div>`;
}
async function loadHeroGrantGearList() {
const raw = (document.getElementById("hero-grant-gear-q") && document.getElementById("hero-grant-gear-q").value) || "";
state.grantGearSearchQuery = raw.trim();
@ -1321,7 +1318,7 @@
render();
if (tab === "constants" && !state.runtime) withAction(loadRuntime);
if (tab === "buffDebuff" && !state.buffDebuff) withAction(loadBuffDebuff);
if (tab === "enemies") withAction(loadContentEnemies);
if (tab === "monsters" && (!state.contentEnemies || state.contentEnemies.length === 0)) withAction(loadContentEnemies);
}
function sectionServer() {
@ -1715,6 +1712,41 @@
</div>`;
}
function sectionMonsters() {
const page = paged(state.contentEnemies || [], "contentEnemies", 15);
const rows = page.items.map(m => `
<tr>
<td>${e(m.id)}</td>
<td><kbd>${e(m.type)}</kbd></td>
<td>${e(m.name)}</td>
<td>${m.isElite ? "★" : "—"}</td>
<td>${e(m.minLevel)}${e(m.maxLevel)}</td>
<td>${e(m.maxHp)}</td>
<td>${e(m.attack)}/${e(m.defense)}</td>
<td>${e(m.speed)}</td>
<td>${e(m.xpReward)}/${e(m.goldReward)}</td>
<td class="muted" style="max-width:180px;overflow:hidden;text-overflow:ellipsis" title="${e((m.specialAbilities || []).join(", "))}">${e((m.specialAbilities || []).join(", ") || "—")}</td>
<td><button class="btn" onclick="openContentEnemyEditorByType(${JSON.stringify(m.type)})">Edit</button></td>
</tr>`).join("");
return `
${contentEnemyEditorHtml()}
<div class="card">
<h3>Монстры (шаблоны)</h3>
<p class="muted">Чтение/запись таблицы <kbd>enemies</kbd>. Изменения применяются к новым встречам; активный бой использует уже созданный экземпляр.</p>
<button class="btn" onclick="withAction(loadContentEnemies)">Обновить из БД</button>
<button class="btn" onclick="withAction(reloadEnemyTemplatesOnly)">Только reload в память (без записи)</button>
</div>
<div class="card">
<table class="table">
<thead><tr>
<th>ID</th><th>type</th><th>name</th><th>elite</th><th>lvl</th><th>HP</th><th>atk/def</th><th>spd</th><th>XP/gold</th><th>abilities</th><th></th>
</tr></thead>
<tbody>${rows || `<tr><td colspan="11" class="muted">Нажмите «Обновить из БД»</td></tr>`}</tbody>
</table>
${pagerHtml("contentEnemies", page.page, page.total)}
</div>`;
}
function sectionQuests() {
const page = paged(state.contentQuests, "contentQuests", 12);
const rows = page.items.map(q => `
@ -1740,45 +1772,6 @@
</div>`;
}
function sectionEnemies() {
const page = paged(state.contentEnemies, "contentEnemies", 15);
const rows = page.items.map(en => {
const abs = (en.specialAbilities || []).join(", ");
const elite = en.isElite ? "elite" : "base";
return `<tr>
<td><kbd title="id в БД">${e(en.id)}</kbd></td>
<td><kbd>${e(en.type)}</kbd></td>
<td>${e(en.name)}</td>
<td>${e(elite)}</td>
<td>${e(en.minLevel)}${e(en.maxLevel)}</td>
<td>${e(en.maxHp)}</td>
<td>${e(en.attack)}/${e(en.defense)}</td>
<td>${e(en.speed)}</td>
<td>${e(en.critChance)}</td>
<td>${e(en.xpReward)}/${e(en.goldReward)}</td>
<td class="muted" style="max-width:180px;overflow:hidden;text-overflow:ellipsis" title="${e(abs)}">${e(abs || "—")}</td>
<td><button class="btn" onclick="openContentEnemyEditorByType(${JSON.stringify(en.type)})">Edit</button></td>
</tr>`;
}).join("");
return `
${contentEnemyEditorHtml()}
<div class="card">
<h3>Монстры (шаблоны врагов)</h3>
<p class="muted">Таблица <kbd>enemies</kbd>: базовые статы, уровни, награды, способности. Изменения после «Save» сразу попадают в память процесса. Новые типы добавляются миграциями/сидом, не из этой формы.</p>
<button class="btn" onclick="withAction(loadContentEnemies)">Обновить из БД</button>
<button class="btn" onclick="withAction(reloadEnemyTemplatesOnly)">Только reload в память</button>
</div>
<div class="card">
<table class="table">
<thead><tr>
<th>ID</th><th>Type</th><th>Name</th><th>Class</th><th>Levels</th><th>maxHP</th><th>Atk/Def</th><th>Spd</th><th>Crit</th><th>XP/Au</th><th>Abilities</th><th></th>
</tr></thead>
<tbody>${rows || `<tr><td colspan="12" class="muted">Нет данных — нажмите «Обновить из БД»</td></tr>`}</tbody>
</table>
${pagerHtml("contentEnemies", page.page, page.total)}
</div>`;
}
function sectionTowns() {
const townsPage = paged(state.questTowns, "towns", 8);
const towns = townsPage.items.map(t => `<div class="list-row" onclick="withAction(() => selectTown(${t.id}))"><strong>${e(t.name)}</strong><span>Lvl ${e(t.levelMin)}-${e(t.levelMax)}</span><span></span><span>ID ${e(t.id)}</span></div>`).join("");
@ -1843,7 +1836,7 @@
if (state.tab === "constants") return sectionConstants();
if (state.tab === "buffDebuff") return sectionBuffDebuff();
if (state.tab === "gear") return sectionGear();
if (state.tab === "enemies") return sectionEnemies();
if (state.tab === "monsters") return sectionMonsters();
if (state.tab === "quests") return sectionQuests();
if (state.tab === "towns") return sectionTowns();
if (state.tab === "payments") return sectionPayments();
@ -1865,9 +1858,10 @@
<button class="${state.tab==='constants'?'active':''}" onclick="setTab('constants')">3. Constants</button>
<button class="${state.tab==='buffDebuff'?'active':''}" onclick="setTab('buffDebuff')">4. Buffs / Debuffs</button>
<button class="${state.tab==='gear'?'active':''}" onclick="setTab('gear')">5. Gear (content)</button>
<button class="${state.tab==='quests'?'active':''}" onclick="setTab('quests')">6. Quests (content)</button>
<button class="${state.tab==='towns'?'active':''}" onclick="setTab('towns')">7. Towns</button>
<button class="${state.tab==='payments'?'active':''}" onclick="setTab('payments')">8. Payments</button>
<button class="${state.tab==='monsters'?'active':''}" onclick="setTab('monsters')">6. Monsters</button>
<button class="${state.tab==='quests'?'active':''}" onclick="setTab('quests')">7. Quests (content)</button>
<button class="${state.tab==='towns'?'active':''}" onclick="setTab('towns')">8. Towns</button>
<button class="${state.tab==='payments'?'active':''}" onclick="setTab('payments')">9. Payments</button>
</aside>
<main class="main">
${state.message ? `<div class="card">${e(state.message)}</div>` : ""}
@ -1889,6 +1883,11 @@
window.toggleLiveSnapshotOpen = toggleLiveSnapshotOpen;
window.toggleHeroQuestWorldOpen = toggleHeroQuestWorldOpen;
window.jsonViewerToggle = jsonViewerToggle;
window.loadContentEnemies = loadContentEnemies;
window.openContentEnemyEditorByType = openContentEnemyEditorByType;
window.closeContentEnemyEditor = closeContentEnemyEditor;
window.saveContentEnemy = saveContentEnemy;
window.reloadEnemyTemplatesOnly = reloadEnemyTemplatesOnly;
render();
</script>
</body>

Loading…
Cancel
Save