Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions _datafiles/html/admin/mobs.html
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ <h2 id="editor-title">Mob Editor</h2>
<div class="buff-ids-wrap" id="buffids-list"></div>
<button class="btn-add-sm" style="margin-top:0.4rem;" onclick="pickBuffId('buffids-list')">+ Add Buff</button>
</div>

</div>

<div class="section-card">
Expand All @@ -361,6 +362,11 @@ <h2 id="editor-title">Mob Editor</h2>
<option value="0">&mdash; none &mdash;</option>
</select>
</div>
<div class="field" id="f-formrace-wrap" style="display:none;">
<label>Current Form</label>
<input type="text" id="f-formrace" readonly
style="background:#f0f0f0; cursor:default;" />
</div>

<div class="field span2">
<label>Description</label>
Expand Down Expand Up @@ -464,6 +470,15 @@ <h2 id="editor-title">Mob Editor</h2>
<div class="field-hint">Items the mob carries</div>
</div>

<div class="section-title" style="margin-top:0.5rem;">Spellbook</div>

<div class="field span2">
<label>Known Spells</label>
<div class="buff-ids-wrap" id="spellbook-list"></div>
<button class="btn-add-sm" style="margin-top:0.4rem;" onclick="pickSpell()">+ Add Spell</button>
<div class="field-hint">Spells this mob knows. Value indicates cast count (positive = enabled, negative = disabled).</div>
</div>

<div class="section-title" style="margin-top:0.5rem;">Shop</div>

<div class="field span2">
Expand Down Expand Up @@ -723,6 +738,14 @@ <h2 id="editor-title">Mob Editor</h2>
setVal('f-description', ch.Description || '');
setVal('f-raceid', ch.RaceId || 0);
applyRaceEquipDisabled(ch.RaceId || 0);
var formWrap = document.getElementById('f-formrace-wrap');
if (ch.FormRaceId > 0) {
var fr = allRaces[String(ch.FormRaceId)];
document.getElementById('f-formrace').value = fr ? fr.Name : 'Unknown (#' + ch.FormRaceId + ')';
formWrap.style.display = '';
} else {
formWrap.style.display = 'none';
}
setVal('f-level', ch.Level || 1);
setVal('f-hostile', mob.Hostile ? 'true' : 'false');

Expand Down Expand Up @@ -753,6 +776,9 @@ <h2 id="editor-title">Mob Editor</h2>
// Buff IDs
renderBuffChips('buffids-list', mob.BuffIds || []);

// Spellbook
renderSpellChips('spellbook-list', ch.SpellBook || {});

// Stats
const st = ch.Stats || {};
setVal('f-stat-strength', (st.Strength && st.Strength.Training) || 0);
Expand Down Expand Up @@ -891,6 +917,53 @@ <h2 id="editor-title">Mob Editor</h2>
return Array.from(document.querySelectorAll('#' + listId + ' .buff-chip')).map(c => parseInt(c.dataset.id, 10));
}

// -------------------------------------------------------------------------
// Spell chips (spellbook)
// -------------------------------------------------------------------------
function renderSpellChips(listId, spellBook) {
const list = document.getElementById(listId);
list.innerHTML = '';
for (const [id, val] of Object.entries(spellBook)) {
appendSpellChip(list, id, val);
}
}

function appendSpellChip(list, spellId, val) {
const chip = document.createElement('span');
chip.className = 'buff-chip';
chip.dataset.id = spellId;
chip.dataset.val = val;
const label = esc(spellId);
chip.innerHTML = `${label} <button title="Remove" onclick="this.closest('.buff-chip').remove()">&times;</button>`;
list.appendChild(chip);
}

window.pickSpell = function () {
const listEl = document.getElementById('spellbook-list');
const existing = collectSpellBook();
const existingIds = Object.keys(existing);
Picker.open({
...PickerConfigs.spells,
multi: true,
selected: existingIds,
onSelect: (spells) => {
listEl.innerHTML = '';
for (const spell of spells) {
const prev = existing[spell.SpellId];
appendSpellChip(listEl, spell.SpellId, prev !== undefined ? prev : 1);
}
},
});
};

function collectSpellBook() {
const book = {};
document.querySelectorAll('#spellbook-list .buff-chip').forEach(c => {
book[c.dataset.id] = parseInt(c.dataset.val, 10) || 1;
});
return book;
}

// -------------------------------------------------------------------------
// Inventory items
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -1242,8 +1315,9 @@ <h2 id="editor-title">Mob Editor</h2>
Legs: eqSlot('f-eq-legs'),
Feet: eqSlot('f-eq-feet'),
},
Items: collectItems(),
Shop: collectShop(),
Items: collectItems(),
Shop: collectShop(),
SpellBook: collectSpellBook(),
},
};

Expand Down
199 changes: 199 additions & 0 deletions _datafiles/html/admin/spells-api.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
{{template "header" .}}

<style>
h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
.subtitle { color: #555; margin-bottom: 2rem; font-size: 0.9rem; }
.api-list { display: flex; flex-direction: column; gap: 0.5rem; }
.api-entry { background: #fff; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
.api-entry summary { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.65rem 1rem; cursor: pointer; user-select: none; list-style: none; }
.api-entry summary::-webkit-details-marker { display: none; }
.api-entry summary::before { content: "\25B6"; font-size: 0.65rem; color: #888; transition: transform 0.15s; flex-shrink: 0; }
.api-entry[open] summary::before { transform: rotate(90deg); }
.api-entry summary:hover { background: #fafafa; }
.method { font-family: monospace; font-size: 0.8rem; font-weight: 700; padding: 0.15rem 0.45rem; border-radius: 3px; flex-shrink: 0; }
.method-get { background: #e6f4ea; color: #1e6e34; }
.method-post { background: #e3f2fd; color: #0d47a1; }
.method-patch { background: #fff3e0; color: #8a4a00; }
.method-put { background: #f3e5f5; color: #6a1b9a; }
.method-delete { background: #fde8e8; color: #8a0000; }
.api-path { font-family: monospace; font-size: 0.9rem; color: #1a1a2e; }
.api-desc { font-size: 0.85rem; color: #555; margin-left: auto; text-align: right; }
.api-body { padding: 0.75rem 1rem 1rem; border-top: 1px solid #eee; background: #fafafa; }
.api-body p { font-size: 0.85rem; color: #444; margin-bottom: 0.5rem; }
.curl-block { background: #1a1a2e; color: #c9d1d9; font-family: monospace; font-size: 0.82rem; padding: 0.85rem 1rem; border-radius: 5px; white-space: pre; overflow-x: auto; line-height: 1.6; margin-bottom: 0.5rem; }
.curl-block .kw { color: #79c0ff; }
.curl-block .str { color: #a5d6ff; }
.curl-block .flag { color: #d2a8ff; }
.response-examples { margin-top: 1rem; display: flex; flex-direction: column; gap: 0.4rem; }
.resp-entry { border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
.resp-entry summary { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.45rem 0.75rem; cursor: pointer; user-select: none; list-style: none; background: #f5f5f5; font-size: 0.82rem; }
.resp-entry summary::-webkit-details-marker { display: none; }
.resp-entry summary::before { content: "\25B6"; font-size: 0.6rem; color: #888; transition: transform 0.15s; flex-shrink: 0; }
.resp-entry[open] summary::before { transform: rotate(90deg); }
.resp-entry summary:hover { background: #efefef; }
.resp-body { padding: 0.6rem 0.75rem 0.75rem; border-top: 1px solid #eee; background: #fafafa; }
.resp-body p { font-size: 0.82rem; color: #444; margin-bottom: 0.4rem; }
.status-ok { background: #e6f4ea; color: #1e6e34; }
.status-err { background: #fde8e8; color: #8a0000; }
.resp-label { font-size: 0.8rem; color: #444; }
</style>

<h1>Spells API Reference</h1>
<p class="subtitle">REST endpoints for browsing, editing, creating, and deleting spell definitions.</p>

<div class="api-list">

<details class="api-entry">
<summary>
<span class="method method-get">GET</span>
<span class="api-path">/admin/api/v2/spells</span>
<span class="api-desc">Return all spell definitions</span>
</summary>
<div class="api-body">
<p>Returns an array of all spell specs with a <code>HasScript</code> boolean.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true,"data":[{"SpellId":"fireball","Name":"Fireball","Type":"harmsingle","School":"conjuration","Cost":10,"WaitRounds":3,"Difficulty":0,"HasScript":true}]}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-get">GET</span>
<span class="api-path">/admin/api/v2/spells/{spellId}</span>
<span class="api-desc">Get a single spell</span>
</summary>
<div class="api-body">
<p><code>{spellId}</code> is the spell's string identifier.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells/fireball</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true,"data":{"SpellId":"fireball","Name":"Fireball","Type":"harmsingle","School":"conjuration","Cost":10,"WaitRounds":3,"Difficulty":0}}</div></div>
</details>
<details class="resp-entry">
<summary><span class="method resp-label status-err">404</span> <span class="resp-label">Not Found</span></summary>
<div class="resp-body"><div class="curl-block">{"success":false,"error":"spell not found: fireball"}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-post">POST</span>
<span class="api-path">/admin/api/v2/spells</span>
<span class="api-desc">Create a new spell</span>
</summary>
<div class="api-body">
<p><code>SpellId</code> is required and must be unique.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="flag">-X</span> POST \
<span class="flag">-H</span> <span class="str">"Content-Type: application/json"</span> \
<span class="flag">-d</span> <span class="str">'{"SpellId":"icebolt","Name":"Ice Bolt","Type":"harmsingle","School":"conjuration","Cost":8}'</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">201</span> <span class="resp-label">Created</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true,"data":{"SpellId":"icebolt","Name":"Ice Bolt","Type":"harmsingle","School":"conjuration","Cost":8,"WaitRounds":0,"Difficulty":0}}</div></div>
</details>
<details class="resp-entry">
<summary><span class="method resp-label status-err">409</span> <span class="resp-label">Conflict</span></summary>
<div class="resp-body"><div class="curl-block">{"success":false,"error":"spell already exists: icebolt"}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-patch">PATCH</span>
<span class="api-path">/admin/api/v2/spells/{spellId}</span>
<span class="api-desc">Update spell properties</span>
</summary>
<div class="api-body">
<p>Merges the supplied fields into the existing spec. The <code>SpellId</code> in the body is ignored.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="flag">-X</span> PATCH \
<span class="flag">-H</span> <span class="str">"Content-Type: application/json"</span> \
<span class="flag">-d</span> <span class="str">'{"Cost":15,"Difficulty":10}'</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells/fireball</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true,"data":{"SpellId":"fireball","Name":"Fireball","Cost":15,"Difficulty":10,...}}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-delete">DELETE</span>
<span class="api-path">/admin/api/v2/spells/{spellId}</span>
<span class="api-desc">Delete a spell and its script</span>
</summary>
<div class="api-body">
<p>Permanently removes the spell YAML and associated JS script from disk and from the in-memory cache.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="flag">-X</span> DELETE \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells/fireball</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-get">GET</span>
<span class="api-path">/admin/api/v2/spells/{spellId}/script</span>
<span class="api-desc">Get the spell's JavaScript</span>
</summary>
<div class="api-body">
<p>Returns the raw script content. <code>script</code> is an empty string if no script file exists.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells/fireball/script</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true,"data":{"script":"function onCast(actor, target){...}"}}</div></div>
</details>
</div>
</div>
</details>

<details class="api-entry">
<summary>
<span class="method method-put">PUT</span>
<span class="api-path">/admin/api/v2/spells/{spellId}/script</span>
<span class="api-desc">Replace (or delete) the spell's JavaScript</span>
</summary>
<div class="api-body">
<p>Writes the provided <code>script</code> string to the spell's <code>.js</code> file. Send an empty string to delete the script file.</p>
<div class="curl-block"><span class="kw">curl</span> <span class="flag">-s</span> <span class="flag">-u</span> <span class="str">admin:password</span> \
<span class="flag">-X</span> PUT \
<span class="flag">-H</span> <span class="str">"Content-Type: application/json"</span> \
<span class="flag">-d</span> <span class="str">'{"script":"function onCast(actor, target){}"}'</span> \
<span class="str">http://{{.CONFIG.FilePaths.WebDomain}}/admin/api/v2/spells/fireball/script</span></div>
<div class="response-examples">
<details class="resp-entry">
<summary><span class="method resp-label status-ok">200</span> <span class="resp-label">Success</span></summary>
<div class="resp-body"><div class="curl-block">{"success":true}</div></div>
</details>
</div>
</div>
</details>

</div>

{{template "footer" .}}
Loading
Loading