diff --git a/admin-web/index.html b/admin-web/index.html index 440f20b..fff3cbd 100644 --- a/admin-web/index.html +++ b/admin-web/index.html @@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); } 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 => - `` - ).join(""); - return ` -
-

Edit enemy: ${e(ed.type)}

-

Запись в таблице enemies. После сохранения сервер подставляет hp = maxHp и перезагружает шаблоны в памяти.

- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Special abilities
-
${abChecks}
- - -
`; + 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 @@ `; } + 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 ` +
+

Edit monster template

+

Таблица enemies. Поле type не меняется (ключ). После сохранения шаблоны подхватываются в память сервера.

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Допустимые теги: ${e(abHint)}

+ + +
`; + } 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 @@ `; } + function sectionMonsters() { + const page = paged(state.contentEnemies || [], "contentEnemies", 15); + const rows = page.items.map(m => ` + + ${e(m.id)} + ${e(m.type)} + ${e(m.name)} + ${m.isElite ? "★" : "—"} + ${e(m.minLevel)}–${e(m.maxLevel)} + ${e(m.maxHp)} + ${e(m.attack)}/${e(m.defense)} + ${e(m.speed)} + ${e(m.xpReward)}/${e(m.goldReward)} + ${e((m.specialAbilities || []).join(", ") || "—")} + + `).join(""); + return ` + ${contentEnemyEditorHtml()} +
+

Монстры (шаблоны)

+

Чтение/запись таблицы enemies. Изменения применяются к новым встречам; активный бой использует уже созданный экземпляр.

+ + +
+
+ + + + + ${rows || ``} +
IDtypenameelitelvlHPatk/defspdXP/goldabilities
Нажмите «Обновить из БД»
+ ${pagerHtml("contentEnemies", page.page, page.total)} +
`; + } + function sectionQuests() { const page = paged(state.contentQuests, "contentQuests", 12); const rows = page.items.map(q => ` @@ -1740,45 +1772,6 @@ `; } - 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 ` - ${e(en.id)} - ${e(en.type)} - ${e(en.name)} - ${e(elite)} - ${e(en.minLevel)}–${e(en.maxLevel)} - ${e(en.maxHp)} - ${e(en.attack)}/${e(en.defense)} - ${e(en.speed)} - ${e(en.critChance)} - ${e(en.xpReward)}/${e(en.goldReward)} - ${e(abs || "—")} - - `; - }).join(""); - return ` - ${contentEnemyEditorHtml()} -
-

Монстры (шаблоны врагов)

-

Таблица enemies: базовые статы, уровни, награды, способности. Изменения после «Save» сразу попадают в память процесса. Новые типы добавляются миграциями/сидом, не из этой формы.

- - -
-
- - - - - ${rows || ``} -
IDTypeNameClassLevelsmaxHPAtk/DefSpdCritXP/AuAbilities
Нет данных — нажмите «Обновить из БД»
- ${pagerHtml("contentEnemies", page.page, page.total)} -
`; - } - function sectionTowns() { const townsPage = paged(state.questTowns, "towns", 8); const towns = townsPage.items.map(t => `
${e(t.name)}Lvl ${e(t.levelMin)}-${e(t.levelMax)}ID ${e(t.id)}
`).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 @@ - - - + + + +
${state.message ? `
${e(state.message)}
` : ""} @@ -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();