Add NPC/monster spawn browser with map and monster names to webtools
Map names now come from LIST_ZONE.STB (row index = map id, matching CWorldServer::LoadZoneData). New NPCs tab lists all placed NPCs from list_npcs with coordinates; new Monsters tab lists all 1,193 named montypes from LIST_NPC.STB. Both are click-to-copy GM commands (/tele, /mon) for quick GM navigation/spawning.
This commit is contained in:
1 parent
16dccc94f2
commit
e0d1f629db
4 files changed
+468
-36
No files matched your search
+135
-6
@@ -175,6 +175,8 @@
|
||||
<div class="tab active" data-view="commands"><span class="ico">⌘</span> GM Commands</div>
|
||||
<div class="tab" data-view="items"><span class="ico">◆</span> Item Configurator</div>
|
||||
<div class="tab" data-view="maps"><span class="ico">⌗</span> Maps</div>
|
||||
<div class="tab" data-view="npcs"><span class="ico">☺</span> NPCs</div>
|
||||
<div class="tab" data-view="monsters"><span class="ico">☠</span> Monsters</div>
|
||||
<a class="tab-link" href="/admin/"><span class="ico">⚙</span> Admin Panel</a>
|
||||
</nav>
|
||||
|
||||
@@ -200,6 +202,22 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="side-panel" id="panel-npcs">
|
||||
<div class="label">Map</div>
|
||||
<select id="npc-map"></select>
|
||||
<div class="label">Search by name</div>
|
||||
<input id="q-npcs" type="text" placeholder="e.g. Lampa" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="side-panel" id="panel-monsters">
|
||||
<div class="label">Spawn count</div>
|
||||
<input id="mon-count" type="number" value="1" min="1" />
|
||||
<div class="label">AI (0 = default)</div>
|
||||
<input id="mon-ai" type="number" value="0" min="0" />
|
||||
<div class="label">Search by name or id</div>
|
||||
<input id="q-monsters" type="text" placeholder="e.g. jelly, or 210" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="legend" id="legend-commands">
|
||||
<div class="row"><span class="pill pill-any">any</span> usable by any GM character</div>
|
||||
<div class="row"><span class="pill pill-gm">gm 300+</span> accesslevel ≥ 300</div>
|
||||
@@ -254,17 +272,39 @@
|
||||
<section class="view" id="view-maps">
|
||||
<div class="pagehead">
|
||||
<h1>Every map with active spawn or NPC data</h1>
|
||||
<p>Aggregated live from <code>list_special_spawns</code> and <code>list_npcs</code>.</p>
|
||||
<p>Names pulled straight from <code>LIST_ZONE.STB</code> (row index = map id, same as <code>CWorldServer::LoadZoneData</code>
|
||||
uses server-side); spawn/NPC counts aggregated live from <code>list_special_spawns</code> and <code>list_npcs</code>.</p>
|
||||
</div>
|
||||
<div class="map-empty-note">
|
||||
No minimap images or display names are shown here — this repo only ships the server side of KTRose, not the
|
||||
client's 3D/texture assets, and the localized zone-name strings aren't parsed by the server's simplified STB
|
||||
loader either. What's below (spawn counts, monster totals, coordinate bounds) is real, queried straight from
|
||||
the running database.
|
||||
No minimap images are shipped here — this repo only ships the server side of KTRose, not the client's 3D/texture
|
||||
assets. A handful of zone names are untranslated Korean (the dev-tool STB just was never localized for those rows).
|
||||
</div>
|
||||
<div class="map-grid" id="results-maps"></div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== NPCS ===================== -->
|
||||
<section class="view" id="view-npcs">
|
||||
<div class="pagehead">
|
||||
<h1>Every placed NPC, with exact coordinates</h1>
|
||||
<p>Straight out of <code>list_npcs</code>. Click a card to copy the matching <code>/tele</code> command
|
||||
and warp straight to it as GM.</p>
|
||||
</div>
|
||||
<div class="resultmeta" id="resultmeta-npcs"></div>
|
||||
<div class="item-grid" id="results-npcs"></div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== MONSTERS ===================== -->
|
||||
<section class="view" id="view-monsters">
|
||||
<div class="pagehead">
|
||||
<h1>Every monster/NPC type, ready to spawn</h1>
|
||||
<p>1,193 named entries straight out of <code>LIST_NPC.STB</code> (row index = <code>montype</code>, the same
|
||||
id <code>/mon</code> and <code>/spawn</code> expect). Click one to copy a ready-to-paste <code>/mon</code>
|
||||
command that drops it next to you.</p>
|
||||
</div>
|
||||
<div class="resultmeta" id="resultmeta-monsters"></div>
|
||||
<div class="item-grid" id="results-monsters"></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -456,7 +496,7 @@ function renderMaps(){
|
||||
? `<div>X range</div><b>${m.bounds.minX} – ${m.bounds.maxX}</b><div>Y range</div><b>${m.bounds.minY} – ${m.bounds.maxY}</b>`
|
||||
: `<div>Coordinates</div><b>no spawn data</b><div></div><b></b>`;
|
||||
el.innerHTML = `
|
||||
<h3>Map ${m.id}</h3>
|
||||
<h3>${m.name || ("Map "+m.id)} <span style="color:var(--ink-faint); font-weight:400;">#${m.id}</span></h3>
|
||||
${m.note ? `<div class="note">${m.note}</div>` : ""}
|
||||
<div class="map-stats">
|
||||
<div>Spawn points</div><b>${m.spawnPoints}</b>
|
||||
@@ -469,6 +509,81 @@ function renderMaps(){
|
||||
}
|
||||
document.getElementById("map-sort").addEventListener("change", renderMaps);
|
||||
|
||||
/* ---------------- NPCS TAB ---------------- */
|
||||
let NPCS = [];
|
||||
function renderNpcs(){
|
||||
const root = document.getElementById("results-npcs");
|
||||
const meta = document.getElementById("resultmeta-npcs");
|
||||
const map = document.getElementById("npc-map").value;
|
||||
const q = document.getElementById("q-npcs").value.trim().toLowerCase();
|
||||
root.innerHTML = "";
|
||||
|
||||
let filtered = NPCS;
|
||||
if(map !== "__all__") filtered = filtered.filter(n => String(n.map) === map);
|
||||
if(q) filtered = filtered.filter(n => n.name.toLowerCase().includes(q));
|
||||
|
||||
filtered.forEach(n=>{
|
||||
const el = document.createElement("div");
|
||||
el.className = "item-card";
|
||||
const tele = `/tele ${n.map} ${n.x} ${n.y}`;
|
||||
el.innerHTML = `<div class="nm">${n.name}</div><div class="id">map ${n.map} · x ${n.x} · y ${n.y}</div>`;
|
||||
el.title = "Click to copy: " + tele;
|
||||
el.addEventListener("click", ()=>{
|
||||
copyText(tele);
|
||||
const nm = el.querySelector(".nm");
|
||||
const prev = nm.textContent;
|
||||
nm.textContent = "Copied: " + tele;
|
||||
setTimeout(()=>{ nm.textContent = prev; }, 1200);
|
||||
});
|
||||
root.appendChild(el);
|
||||
});
|
||||
|
||||
if(filtered.length===0) root.innerHTML = `<div class="empty">No NPC matches.</div>`;
|
||||
meta.textContent = `${filtered.length} of ${NPCS.length} NPCs`;
|
||||
}
|
||||
document.getElementById("q-npcs").addEventListener("input", renderNpcs);
|
||||
document.getElementById("npc-map").addEventListener("change", renderNpcs);
|
||||
|
||||
/* ---------------- MONSTERS TAB ---------------- */
|
||||
let MONSTERS = [];
|
||||
function renderMonsters(){
|
||||
const root = document.getElementById("results-monsters");
|
||||
const meta = document.getElementById("resultmeta-monsters");
|
||||
const q = document.getElementById("q-monsters").value.trim().toLowerCase();
|
||||
root.innerHTML = "";
|
||||
|
||||
let filtered = MONSTERS;
|
||||
if(q) filtered = filtered.filter(m => m.name.toLowerCase().includes(q) || String(m.id)===q);
|
||||
const CAP = 180;
|
||||
const total = filtered.length;
|
||||
const shown = filtered.slice(0, CAP);
|
||||
|
||||
shown.forEach(m=>{
|
||||
const el = document.createElement("div");
|
||||
el.className = "item-card";
|
||||
el.innerHTML = `<div class="nm">${m.name}</div><div class="id">montype ${m.id}</div>`;
|
||||
el.addEventListener("click", ()=>{
|
||||
const count = document.getElementById("mon-count").value || 1;
|
||||
const ai = document.getElementById("mon-ai").value || 0;
|
||||
const cmd = `/mon ${m.id} ${count} ${ai}`;
|
||||
copyText(cmd);
|
||||
const nm = el.querySelector(".nm");
|
||||
const prev = nm.textContent;
|
||||
nm.textContent = "Copied: " + cmd;
|
||||
setTimeout(()=>{ nm.textContent = prev; }, 1200);
|
||||
});
|
||||
root.appendChild(el);
|
||||
});
|
||||
|
||||
if(total===0) root.innerHTML = `<div class="empty">No monster matches.</div>`;
|
||||
meta.textContent = total > CAP
|
||||
? `Showing first ${CAP} of ${total} matches — narrow your search to see more.`
|
||||
: `${total} of ${MONSTERS.length} monster types`;
|
||||
}
|
||||
document.getElementById("q-monsters").addEventListener("input", renderMonsters);
|
||||
document.getElementById("mon-count").addEventListener("input", renderMonsters);
|
||||
document.getElementById("mon-ai").addEventListener("input", renderMonsters);
|
||||
|
||||
/* ---------------- boot ---------------- */
|
||||
buildCommandsNav();
|
||||
renderCommands("");
|
||||
@@ -485,6 +600,20 @@ fetch("maps.json").then(r=>r.json()).then(data=>{
|
||||
MAPS = data;
|
||||
renderMaps();
|
||||
});
|
||||
|
||||
fetch("npcs.json").then(r=>r.json()).then(data=>{
|
||||
NPCS = data;
|
||||
const maps = [...new Set(data.map(n=>n.map))].sort((a,b)=>a-b);
|
||||
const sel = document.getElementById("npc-map");
|
||||
sel.innerHTML = `<option value="__all__">All maps</option>` +
|
||||
maps.map(m=>`<option value="${m}">Map ${m}</option>`).join("");
|
||||
renderNpcs();
|
||||
});
|
||||
|
||||
fetch("monsters.json").then(r=>r.json()).then(data=>{
|
||||
MONSTERS = data;
|
||||
renderMonsters();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user