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.
620 lines
29 KiB
HTML
620 lines
29 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>KTRose GM Toolbox</title>
|
||
<style>
|
||
:root{
|
||
--bg:#12141a; --bg-raised:#181b23; --bg-card:#1d2029; --line:#2a2e3a;
|
||
--ink:#e9e6e2; --ink-dim:#9a99a8; --ink-faint:#65647a;
|
||
--thorn:#c2557a; --thorn-dim:#8a4560; --thorn-ink:#e28aa8;
|
||
--leaf:#7fb88a; --amber:#d1a054; --danger:#d16060;
|
||
--mono: ui-monospace,"Cascadia Code","SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
|
||
--serif: Georgia,"Iowan Old Style","Palatino Linotype","Book Antiqua",serif;
|
||
--sans: -apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||
--radius:10px;
|
||
}
|
||
@media (prefers-color-scheme: light){
|
||
:root:not([data-theme="dark"]){
|
||
--bg:#f7f4ef; --bg-raised:#ffffff; --bg-card:#ffffff; --line:#e2ddd3;
|
||
--ink:#231d22; --ink-dim:#645d68; --ink-faint:#8e8790;
|
||
--thorn:#a83060; --thorn-dim:#c98aa2; --thorn-ink:#8a1f45;
|
||
--leaf:#3e7a4d; --amber:#8a611f; --danger:#a83a3a;
|
||
}
|
||
}
|
||
:root[data-theme="light"]{
|
||
--bg:#f7f4ef; --bg-raised:#ffffff; --bg-card:#ffffff; --line:#e2ddd3;
|
||
--ink:#231d22; --ink-dim:#645d68; --ink-faint:#8e8790;
|
||
--thorn:#a83060; --thorn-dim:#c98aa2; --thorn-ink:#8a1f45;
|
||
--leaf:#3e7a4d; --amber:#8a611f; --danger:#a83a3a;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
html,body{margin:0;padding:0;}
|
||
body{
|
||
background:radial-gradient(1200px 600px at 15% -10%, color-mix(in srgb, var(--thorn) 10%, transparent), transparent) var(--bg);
|
||
color:var(--ink); font-family:var(--sans); -webkit-font-smoothing:antialiased;
|
||
}
|
||
a{color:inherit;}
|
||
::selection{background:color-mix(in srgb, var(--thorn) 35%, transparent);}
|
||
|
||
.shell{display:grid; grid-template-columns:260px 1fr; min-height:100vh;}
|
||
@media (max-width:860px){.shell{grid-template-columns:1fr;}}
|
||
|
||
.side{
|
||
border-right:1px solid var(--line); background:var(--bg-raised);
|
||
padding:26px 20px; position:sticky; top:0; height:100vh; overflow-y:auto;
|
||
display:flex; flex-direction:column; gap:20px;
|
||
}
|
||
@media (max-width:860px){.side{position:static; height:auto; border-right:none; border-bottom:1px solid var(--line);}}
|
||
|
||
.brand .mark{
|
||
font-family:var(--serif); font-size:1.4rem; font-style:italic;
|
||
background:linear-gradient(120deg, var(--thorn), var(--ink) 140%);
|
||
-webkit-background-clip:text; background-clip:text; color:transparent;
|
||
}
|
||
.brand .sub{font-size:0.7rem; letter-spacing:0.1em; text-transform:uppercase; color:var(--ink-faint); margin-top:2px;}
|
||
|
||
.tabs{display:flex; flex-direction:column; gap:3px;}
|
||
.tab, .tab-link{
|
||
display:flex; align-items:center; gap:9px; padding:9px 11px; border-radius:8px;
|
||
font-size:0.9rem; color:var(--ink-dim); cursor:pointer; border:1px solid transparent;
|
||
text-decoration:none;
|
||
}
|
||
.tab:hover, .tab-link:hover{background:var(--bg-card); color:var(--ink);}
|
||
.tab.active{background:var(--bg-card); border-color:var(--line); color:var(--ink); font-weight:600;}
|
||
.tab .ico, .tab-link .ico{font-size:1rem; width:1.2em; text-align:center;}
|
||
.tab-link{border-top:1px solid var(--line); margin-top:6px; padding-top:12px;}
|
||
|
||
.side-panel{display:none; flex-direction:column; gap:10px;}
|
||
.side-panel.active{display:flex;}
|
||
.side-panel input, .side-panel select{
|
||
width:100%; background:var(--bg-card); border:1px solid var(--line); color:var(--ink);
|
||
padding:9px 11px; border-radius:8px; font-size:0.87rem; font-family:var(--sans);
|
||
}
|
||
.side-panel input:focus, .side-panel select:focus{outline:2px solid var(--thorn); outline-offset:1px;}
|
||
.side-panel .label{font-size:0.68rem; letter-spacing:0.1em; text-transform:uppercase; color:var(--ink-faint); margin:6px 0 -2px;}
|
||
|
||
.navgroup{display:flex; flex-direction:column; gap:2px; max-height:340px; overflow-y:auto;}
|
||
.navlink{
|
||
display:flex; align-items:center; justify-content:space-between; padding:6px 10px;
|
||
border-radius:7px; font-size:0.83rem; color:var(--ink-dim); cursor:pointer; border:1px solid transparent;
|
||
}
|
||
.navlink:hover{background:var(--bg-card); color:var(--ink);}
|
||
.navlink.on{background:var(--bg-card); border-color:var(--line); color:var(--ink); font-weight:600;}
|
||
.navlink .count{font-variant-numeric:tabular-nums; font-size:0.7rem; color:var(--ink-faint);}
|
||
|
||
.legend{margin-top:auto; padding-top:14px; border-top:1px solid var(--line); font-size:0.7rem; color:var(--ink-faint); display:flex; flex-direction:column; gap:6px;}
|
||
.legend .row{display:flex; align-items:center; gap:8px;}
|
||
.pill{font-size:0.66rem; letter-spacing:0.06em; text-transform:uppercase; padding:2px 8px; border-radius:999px; font-weight:700; white-space:nowrap;}
|
||
.pill-gm{background:color-mix(in srgb, var(--leaf) 22%, var(--bg-card)); color:var(--leaf);}
|
||
.pill-dev{background:color-mix(in srgb, var(--amber) 25%, var(--bg-card)); color:var(--amber);}
|
||
.pill-any{background:color-mix(in srgb, var(--ink-faint) 20%, var(--bg-card)); color:var(--ink-dim);}
|
||
|
||
.main{padding:34px 42px 80px; max-width:1040px;}
|
||
@media (max-width:640px){.main{padding:22px 16px 60px;}}
|
||
.view{display:none;}
|
||
.view.active{display:block;}
|
||
|
||
.pagehead{margin-bottom:26px;}
|
||
.pagehead h1{font-family:var(--serif); font-style:italic; font-weight:400; font-size:1.9rem; margin:0 0 8px; text-wrap:balance;}
|
||
.pagehead p{margin:0; color:var(--ink-dim); font-size:0.93rem; max-width:66ch; line-height:1.55;}
|
||
.pagehead code{font-family:var(--mono); background:var(--bg-card); border:1px solid var(--line); padding:1px 6px; border-radius:5px; font-size:0.85em;}
|
||
|
||
.resultmeta{font-size:0.78rem; color:var(--ink-faint); margin:20px 0 10px;}
|
||
|
||
.category h2{
|
||
font-size:0.74rem; letter-spacing:0.11em; text-transform:uppercase; color:var(--thorn-dim);
|
||
margin:28px 0 12px; padding-bottom:8px; border-bottom:1px solid var(--line);
|
||
}
|
||
.cmdlist{display:flex; flex-direction:column; gap:9px;}
|
||
.cmd{background:var(--bg-card); border:1px solid var(--line); border-radius:var(--radius); padding:13px 15px; display:flex; flex-direction:column; gap:7px;}
|
||
.cmd-row{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;}
|
||
.cmd-syntax{font-family:var(--mono); font-size:0.9rem; font-weight:600;}
|
||
.cmd-syntax .arg{color:var(--ink-dim); font-weight:400;}
|
||
.copybtn{margin-left:auto; font-size:0.7rem; color:var(--ink-faint); border:1px solid var(--line); background:transparent; border-radius:6px; padding:3px 8px; cursor:pointer; font-family:var(--sans);}
|
||
.copybtn:hover{color:var(--ink); border-color:var(--ink-faint);}
|
||
.copybtn:focus-visible{outline:2px solid var(--thorn); outline-offset:1px;}
|
||
.cmd-desc{font-size:0.86rem; color:var(--ink-dim); line-height:1.5; max-width:70ch;}
|
||
.cmd-example{font-family:var(--mono); font-size:0.79rem; color:var(--thorn-ink); background:color-mix(in srgb, var(--thorn) 10%, transparent); border-left:2px solid var(--thorn); padding:6px 10px; border-radius:0 6px 6px 0; width:fit-content;}
|
||
|
||
/* Items */
|
||
.item-toolbar{display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:16px;}
|
||
.item-toolbar select, .item-toolbar input{
|
||
background:var(--bg-card); border:1px solid var(--line); color:var(--ink); padding:8px 10px; border-radius:8px; font-size:0.85rem;
|
||
}
|
||
.item-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:8px;}
|
||
.item-card{
|
||
background:var(--bg-card); border:1px solid var(--line); border-radius:9px; padding:10px 12px;
|
||
cursor:pointer; display:flex; flex-direction:column; gap:3px;
|
||
}
|
||
.item-card:hover{border-color:var(--thorn-dim);}
|
||
.item-card .nm{font-size:0.87rem; font-weight:600;}
|
||
.item-card .id{font-family:var(--mono); font-size:0.72rem; color:var(--ink-faint);}
|
||
.item-card.sel{border-color:var(--thorn); background:color-mix(in srgb, var(--thorn) 8%, var(--bg-card));}
|
||
|
||
.configurator{
|
||
position:sticky; bottom:0; margin-top:20px; background:var(--bg-raised); border:1px solid var(--line);
|
||
border-radius:var(--radius); padding:16px 18px; box-shadow:0 -8px 24px -12px rgba(0,0,0,0.4);
|
||
}
|
||
.configurator h3{margin:0 0 10px; font-size:0.95rem; font-family:var(--serif); font-style:italic; font-weight:400;}
|
||
.cfg-grid{display:grid; grid-template-columns:repeat(6,1fr); gap:10px;}
|
||
@media (max-width:760px){.cfg-grid{grid-template-columns:repeat(2,1fr);}}
|
||
.cfg-field label{display:block; font-size:0.66rem; letter-spacing:0.08em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:4px;}
|
||
.cfg-field input, .cfg-field select{width:100%; background:var(--bg-card); border:1px solid var(--line); color:var(--ink); padding:7px 9px; border-radius:7px; font-size:0.85rem;}
|
||
.cfg-out{display:flex; align-items:center; gap:10px; margin-top:14px; flex-wrap:wrap;}
|
||
.cfg-out code{font-family:var(--mono); background:var(--bg-card); border:1px solid var(--line); padding:7px 12px; border-radius:7px; font-size:0.86rem; color:var(--thorn-ink); flex:1; min-width:200px;}
|
||
.cfg-out button{background:var(--thorn); color:#fff; border:none; padding:8px 16px; border-radius:7px; font-size:0.85rem; cursor:pointer; font-weight:600;}
|
||
.cfg-out button:hover{filter:brightness(1.08);}
|
||
.cfg-empty{color:var(--ink-faint); font-size:0.85rem; padding:8px 0;}
|
||
|
||
/* Maps */
|
||
.map-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:12px;}
|
||
.map-card{background:var(--bg-card); border:1px solid var(--line); border-radius:var(--radius); padding:16px;}
|
||
.map-card h3{margin:0 0 4px; font-size:1.1rem; font-family:var(--mono);}
|
||
.map-card .note{font-size:0.78rem; color:var(--thorn-dim); margin:4px 0 8px;}
|
||
.map-stats{display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:0.82rem; color:var(--ink-dim);}
|
||
.map-stats b{color:var(--ink); font-variant-numeric:tabular-nums;}
|
||
.map-empty-note{
|
||
background:color-mix(in srgb, var(--amber) 12%, var(--bg-card)); border:1px solid color-mix(in srgb, var(--amber) 35%, var(--line));
|
||
border-radius:var(--radius); padding:14px 16px; font-size:0.85rem; color:var(--ink-dim); margin-bottom:20px; line-height:1.5;
|
||
}
|
||
|
||
.empty{padding:50px 20px; text-align:center; color:var(--ink-faint); font-size:0.9rem;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
<aside class="side">
|
||
<div class="brand">
|
||
<div class="mark">KTRose Toolbox</div>
|
||
<div class="sub">self-hosted · no auth</div>
|
||
</div>
|
||
|
||
<nav class="tabs">
|
||
<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>
|
||
|
||
<div class="side-panel active" id="panel-commands">
|
||
<div class="label">Search</div>
|
||
<input id="q-commands" type="text" placeholder="Search commands…" autocomplete="off" />
|
||
<div class="navgroup" id="nav-commands"></div>
|
||
</div>
|
||
|
||
<div class="side-panel" id="panel-items">
|
||
<div class="label">Category</div>
|
||
<select id="item-cat"></select>
|
||
<div class="label">Search by name or id</div>
|
||
<input id="q-items" type="text" placeholder="e.g. wooden sword, or 42" autocomplete="off" />
|
||
</div>
|
||
|
||
<div class="side-panel" id="panel-maps">
|
||
<div class="label">Sort</div>
|
||
<select id="map-sort">
|
||
<option value="id">Map ID</option>
|
||
<option value="monsters">Monster count</option>
|
||
<option value="npcs">NPC count</option>
|
||
</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>
|
||
<div class="row"><span class="pill pill-dev">dev 900+</span> accesslevel ≥ 900 + IsDev</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="main">
|
||
|
||
<!-- ===================== COMMANDS ===================== -->
|
||
<section class="view active" id="view-commands">
|
||
<div class="pagehead">
|
||
<h1>Every <code>/command</code> the world server understands</h1>
|
||
<p>Typed straight into normal chat, leading slash included — e.g. <code>/level 50 thomas</code>.
|
||
Pulled directly from <code>gmcmds.cpp</code>. Thomas is flagged accesslevel 900 + IsGM + IsDev,
|
||
so everything here is available to that character.</p>
|
||
</div>
|
||
<div class="resultmeta" id="resultmeta-commands"></div>
|
||
<div id="results-commands"></div>
|
||
</section>
|
||
|
||
<!-- ===================== ITEMS ===================== -->
|
||
<section class="view" id="view-items">
|
||
<div class="pagehead">
|
||
<h1>Pick an item, get the exact <code>/item</code> command</h1>
|
||
<p>9,951 items straight out of <code>itemlist.xls</code> (the same catalog the client uses), grouped into
|
||
the 14 categories the <code>/item</code> and <code>/give2</code> commands expect. Click one to configure
|
||
refine/count/etc. and copy the ready-to-paste command.</p>
|
||
</div>
|
||
<div class="item-toolbar">
|
||
<input id="give-to" type="text" placeholder="Leave empty for yourself, or type a charname for /give2" style="flex:1; min-width:240px;" />
|
||
</div>
|
||
<div class="resultmeta" id="resultmeta-items"></div>
|
||
<div class="item-grid" id="results-items"></div>
|
||
<div class="configurator" id="configurator" style="display:none;">
|
||
<h3 id="cfg-title"></h3>
|
||
<div class="cfg-grid">
|
||
<div class="cfg-field"><label>Count</label><input id="cfg-amount" type="number" value="1" min="1" /></div>
|
||
<div class="cfg-field"><label>Refine (0-9)</label><input id="cfg-refine" type="number" value="0" min="0" max="9" /></div>
|
||
<div class="cfg-field"><label>Lifespan</label><input id="cfg-life" type="number" value="100" min="0" /></div>
|
||
<div class="cfg-field"><label>Socketed</label><select id="cfg-socket"><option value="0">no</option><option value="1">yes</option></select></div>
|
||
<div class="cfg-field"><label>Stats</label><input id="cfg-stats" type="number" value="0" min="0" /></div>
|
||
</div>
|
||
<div class="cfg-out">
|
||
<code id="cfg-command"></code>
|
||
<button id="cfg-copy" type="button">Copy command</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== MAPS ===================== -->
|
||
<section class="view" id="view-maps">
|
||
<div class="pagehead">
|
||
<h1>Every map with active spawn or NPC data</h1>
|
||
<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 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>
|
||
|
||
<script src="commands-data.js"></script>
|
||
<script>
|
||
/* ---------------- shared helpers ---------------- */
|
||
function copyText(t){ navigator.clipboard.writeText(t).catch(()=>{}); }
|
||
|
||
/* ---------------- tab switching ---------------- */
|
||
document.querySelectorAll(".tab").forEach(tab=>{
|
||
tab.addEventListener("click", ()=>{
|
||
document.querySelectorAll(".tab").forEach(t=>t.classList.remove("active"));
|
||
document.querySelectorAll(".side-panel").forEach(p=>p.classList.remove("active"));
|
||
document.querySelectorAll(".view").forEach(v=>v.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
const view = tab.dataset.view;
|
||
document.getElementById("panel-"+view).classList.add("active");
|
||
document.getElementById("view-"+view).classList.add("active");
|
||
document.getElementById("legend-commands").style.display = view==="commands" ? "flex" : "none";
|
||
});
|
||
});
|
||
|
||
/* ---------------- COMMANDS TAB ---------------- */
|
||
function levelClass(l){ if(l.startsWith("dev")) return "pill-dev"; if(l.startsWith("gm")) return "pill-gm"; return "pill-any"; }
|
||
function syntaxHTML(item){
|
||
let out = `<span>${item.cmd}</span>`;
|
||
item.args.forEach(a=>{ out += ` <span class="arg">${a}</span>`; });
|
||
return out;
|
||
}
|
||
function renderCommands(filter){
|
||
const root = document.getElementById("results-commands");
|
||
const meta = document.getElementById("resultmeta-commands");
|
||
root.innerHTML = "";
|
||
let total=0, shown=0;
|
||
const f=(filter||"").trim().toLowerCase();
|
||
COMMANDS.forEach(group=>{
|
||
const filtered = group.items.filter(it=>{
|
||
total++;
|
||
if(!f) return true;
|
||
return (it.cmd+" "+it.args.join(" ")+" "+it.desc+" "+it.ex).toLowerCase().includes(f);
|
||
});
|
||
if(filtered.length===0) return;
|
||
shown += filtered.length;
|
||
const catEl = document.createElement("div");
|
||
catEl.className="category";
|
||
catEl.id="cat-"+group.cat.replace(/\W+/g,"-");
|
||
catEl.innerHTML = `<h2>${group.cat}</h2>`;
|
||
const list = document.createElement("div");
|
||
list.className="cmdlist";
|
||
filtered.forEach(it=>{
|
||
const el = document.createElement("div");
|
||
el.className="cmd";
|
||
el.innerHTML = `
|
||
<div class="cmd-row">
|
||
<span class="cmd-syntax">${syntaxHTML(it)}</span>
|
||
<span class="pill ${levelClass(it.level)}">${it.level}</span>
|
||
<button class="copybtn" type="button">copy</button>
|
||
</div>
|
||
<div class="cmd-desc">${it.desc}</div>
|
||
<div class="cmd-example">${it.ex}</div>`;
|
||
el.querySelector(".copybtn").addEventListener("click",(e)=>{
|
||
copyText(it.ex); e.target.textContent="copied"; setTimeout(()=>{e.target.textContent="copy";},1200);
|
||
});
|
||
list.appendChild(el);
|
||
});
|
||
catEl.appendChild(list);
|
||
root.appendChild(catEl);
|
||
});
|
||
if(shown===0) root.innerHTML = `<div class="empty">No command matches "${filter}".</div>`;
|
||
meta.textContent = f ? `${shown} of ${total} commands match` : `${total} commands across ${COMMANDS.length} categories`;
|
||
}
|
||
function buildCommandsNav(){
|
||
const nav = document.getElementById("nav-commands");
|
||
COMMANDS.forEach(group=>{
|
||
const link = document.createElement("div");
|
||
link.className="navlink";
|
||
link.innerHTML = `<span>${group.cat}</span><span class="count">${group.items.length}</span>`;
|
||
link.addEventListener("click", ()=>{
|
||
document.getElementById("q-commands").value="";
|
||
renderCommands("");
|
||
const t = document.getElementById("cat-"+group.cat.replace(/\W+/g,"-"));
|
||
if(t) t.scrollIntoView({behavior:"smooth", block:"start"});
|
||
});
|
||
nav.appendChild(link);
|
||
});
|
||
}
|
||
document.getElementById("q-commands").addEventListener("input", e=>renderCommands(e.target.value));
|
||
|
||
/* ---------------- ITEMS TAB ---------------- */
|
||
let ITEMS = {};
|
||
let selectedItem = null;
|
||
|
||
function fmtCmd(itemtype, itemnum){
|
||
const amount = document.getElementById("cfg-amount").value || 1;
|
||
const refine = document.getElementById("cfg-refine").value || 0;
|
||
const life = document.getElementById("cfg-life").value || 100;
|
||
const socket = document.getElementById("cfg-socket").value || 0;
|
||
const stats = document.getElementById("cfg-stats").value || 0;
|
||
const target = document.getElementById("give-to").value.trim();
|
||
if(target){
|
||
return `/give2 ${target} ${itemnum} ${itemtype} ${amount} ${refine} ${life} ${socket} ${stats}`;
|
||
}
|
||
return `/item ${itemnum} ${itemtype} ${amount} ${refine} ${life} ${socket} ${stats}`;
|
||
}
|
||
|
||
function openConfigurator(itemtype, itemnum, name){
|
||
selectedItem = {itemtype, itemnum, name};
|
||
document.getElementById("configurator").style.display = "block";
|
||
document.getElementById("cfg-title").textContent = `${name} · itemtype ${itemtype} / itemnum ${itemnum}`;
|
||
updateConfiguratorOutput();
|
||
document.querySelectorAll(".item-card").forEach(c=>c.classList.remove("sel"));
|
||
const el = document.querySelector(`.item-card[data-itemtype="${itemtype}"][data-itemnum="${itemnum}"]`);
|
||
if(el) el.classList.add("sel");
|
||
}
|
||
function updateConfiguratorOutput(){
|
||
if(!selectedItem) return;
|
||
document.getElementById("cfg-command").textContent = fmtCmd(selectedItem.itemtype, selectedItem.itemnum);
|
||
}
|
||
["cfg-amount","cfg-refine","cfg-life","cfg-socket","cfg-stats"].forEach(id=>{
|
||
document.getElementById(id).addEventListener("input", updateConfiguratorOutput);
|
||
});
|
||
document.getElementById("give-to").addEventListener("input", updateConfiguratorOutput);
|
||
document.getElementById("cfg-copy").addEventListener("click", (e)=>{
|
||
copyText(document.getElementById("cfg-command").textContent);
|
||
e.target.textContent = "copied!"; setTimeout(()=>{e.target.textContent="Copy command";},1200);
|
||
});
|
||
|
||
function renderItems(){
|
||
const root = document.getElementById("results-items");
|
||
const meta = document.getElementById("resultmeta-items");
|
||
const cat = document.getElementById("item-cat").value;
|
||
const q = document.getElementById("q-items").value.trim().toLowerCase();
|
||
root.innerHTML = "";
|
||
|
||
if(!q && cat === "__all__"){
|
||
meta.textContent = "Pick a category or search by name/id to browse — 9,951 items total.";
|
||
return;
|
||
}
|
||
|
||
let pool = [];
|
||
const cats = cat === "__all__" ? Object.keys(ITEMS) : [cat];
|
||
cats.forEach(k=>{
|
||
ITEMS[k].items.forEach(([id,name])=>pool.push({itemtype:+k, id, name, catname:ITEMS[k].name}));
|
||
});
|
||
|
||
let filtered = pool;
|
||
if(q){
|
||
filtered = pool.filter(it => it.name.toLowerCase().includes(q) || String(it.id)===q);
|
||
}
|
||
const CAP = 180;
|
||
const total = filtered.length;
|
||
const shown = filtered.slice(0, CAP);
|
||
|
||
shown.forEach(it=>{
|
||
const el = document.createElement("div");
|
||
el.className = "item-card";
|
||
el.dataset.itemtype = it.itemtype;
|
||
el.dataset.itemnum = it.id;
|
||
el.innerHTML = `<div class="nm">${it.name}</div><div class="id">type ${it.itemtype} · id ${it.id}</div>`;
|
||
el.addEventListener("click", ()=>openConfigurator(it.itemtype, it.id, it.name));
|
||
root.appendChild(el);
|
||
});
|
||
|
||
if(total===0){
|
||
root.innerHTML = `<div class="empty">No item matches.</div>`;
|
||
}
|
||
meta.textContent = total > CAP
|
||
? `Showing first ${CAP} of ${total} matches — narrow your search to see more.`
|
||
: `${total} item${total===1?"":"s"}`;
|
||
}
|
||
document.getElementById("q-items").addEventListener("input", renderItems);
|
||
document.getElementById("item-cat").addEventListener("change", renderItems);
|
||
|
||
/* ---------------- MAPS TAB ---------------- */
|
||
let MAPS = [];
|
||
function renderMaps(){
|
||
const root = document.getElementById("results-maps");
|
||
const sort = document.getElementById("map-sort").value;
|
||
const sorted = [...MAPS].sort((a,b)=>{
|
||
if(sort==="monsters") return b.totalMonsters - a.totalMonsters;
|
||
if(sort==="npcs") return b.npcCount - a.npcCount;
|
||
return a.id - b.id;
|
||
});
|
||
root.innerHTML = "";
|
||
sorted.forEach(m=>{
|
||
const el = document.createElement("div");
|
||
el.className = "map-card";
|
||
const bounds = m.bounds
|
||
? `<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>${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>
|
||
<div>Monsters (max pop.)</div><b>${m.totalMonsters}</b>
|
||
<div>NPCs</div><b>${m.npcCount}</b>
|
||
${bounds}
|
||
</div>`;
|
||
root.appendChild(el);
|
||
});
|
||
}
|
||
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("");
|
||
|
||
fetch("items.json").then(r=>r.json()).then(data=>{
|
||
ITEMS = data;
|
||
const sel = document.getElementById("item-cat");
|
||
sel.innerHTML = `<option value="__all__">All categories</option>` +
|
||
Object.keys(data).sort((a,b)=>+a-+b).map(k=>`<option value="${k}">${data[k].name} (${data[k].items.length})</option>`).join("");
|
||
renderItems();
|
||
});
|
||
|
||
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>
|