aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-01 04:53:56 -0400
committerhistoria <[not public]>2026-07-01 04:53:56 -0400
commitbf7c27c2cbf3eb56a0a4c5141e39b152c51614b5 (patch)
tree5383c325fafeb34c01ea737a7070402f040fc5af /internal/admin/static
parentfbe5090ac3325ff8ea6989cdf4515c7f077c975b (diff)
downloadthehouseoficarus-bf7c27c2cbf3eb56a0a4c5141e39b152c51614b5.tar.gz
feat: admin gui map, item, and objects mostly complete
Diffstat (limited to 'internal/admin/static')
-rw-r--r--internal/admin/static/admin.css4
-rw-r--r--internal/admin/static/colorpicker.js7
-rw-r--r--internal/admin/static/itemeditor.js84
-rw-r--r--internal/admin/static/map.js40
-rw-r--r--internal/admin/static/objecteditor.js47
5 files changed, 161 insertions, 21 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index bdf9b60..93c1924 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -148,7 +148,7 @@ g.ud-hover:hover text{font-weight:bold}
.panel-tab-content .search-input{width:100%;padding:4px 6px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace}
.panel-tab-content .search-results{position:absolute;top:100%;left:0;right:0;max-height:160px;overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:3px;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.4)}
.panel-tab-content .search-results .sr-item{padding:4px 8px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03)}
-.panel-tab-content .search-results .sr-item:hover{background:var(--accent)}
+.panel-tab-content .search-results .sr-item:hover,.panel-tab-content .search-results .sr-item.active{background:var(--accent)}
.panel-tab-content .section-add-btn{display:block;width:100%;margin-top:4px;background:var(--accent);color:var(--text);border:1px solid #1a5a8e;padding:4px 8px;font-size:11px;border-radius:3px;cursor:pointer;font-family:monospace}
.panel-tab-content .section-add-btn:hover{background:var(--hover)}
.panel-tab-content .mini-label{font-size:9px;color:#888;display:block;margin-bottom:1px;margin-top:3px}
@@ -201,7 +201,7 @@ g.ud-hover:hover text{font-weight:bold}
.obj-search{position:relative!important}
.obj-search .search-results{position:absolute;top:100%;left:0;right:0;max-height:160px;overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:3px;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.4);margin-top:2px}
.obj-search .search-results .sr-item{padding:4px 8px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03)}
-.obj-search .search-results .sr-item:hover{background:var(--accent)}
+.obj-search .search-results .sr-item:hover,.obj-search .search-results .sr-item.active{background:var(--accent)}
.obj-search input.search-input{padding-right:24px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center}
.obj-search input.search-input::placeholder{color:#666}
.obj-narrow.obj-search input{width:80px}
diff --git a/internal/admin/static/colorpicker.js b/internal/admin/static/colorpicker.js
index 9431e83..bba438e 100644
--- a/internal/admin/static/colorpicker.js
+++ b/internal/admin/static/colorpicker.js
@@ -62,6 +62,7 @@ window.cssToXtermIdx = function(css) {
(function() {
var pickerEl = null, boundInput = null, boundSwatch = null;
+ window._colorPickerActive = false;
function ensurePicker() {
if (pickerEl) return;
@@ -114,6 +115,7 @@ window.cssToXtermIdx = function(css) {
idx = parseInt(raw, 16) || 0;
}
var hex = xtermIdxToHex(idx);
+ window._colorPickerActive = true;
inputEl.value = hex;
if (swatchEl) swatchEl.style.background = xtermToCss(idx);
@@ -153,7 +155,12 @@ window.cssToXtermIdx = function(css) {
window.hideColorPicker = hidePicker;
function hidePicker() {
if (pickerEl) pickerEl.style.display = 'none';
+ var input = boundInput;
boundInput = null; boundSwatch = null;
+ window._colorPickerActive = false;
+ if (input && input.isConnected) {
+ input.dispatchEvent(new Event('input', {bubbles:true}));
+ }
}
document.addEventListener('click', function(e) {
diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js
index d02945a..7c7c676 100644
--- a/internal/admin/static/itemeditor.js
+++ b/internal/admin/static/itemeditor.js
@@ -48,10 +48,10 @@ var SECTIONS = [
label: 'Recipes to Craft This Item', key: 'craft',
fields: [
['type', 'Type', 'select', 'smithing|cooking|crafting|fletching|combine|pharmacy|construction'],
- ['subtype', 'Subtype', 'select', '|smelt|clean'],
+ ['subtype', 'Subtype', 'select', '|smelting|cleaning'],
['level', 'Level', 'number', '', 'narrow'],
['xp', 'XP', 'number', '', 'narrow'],
- ['wait', 'Wait', 'number', '', 'narrow'],
+ ['ticks_per_cycle', 'Ticks/Cycle', 'number', '', 'narrow'],
['output_qty', 'Output Qty', 'number', '', 'narrow'],
['station', 'Station', 'select', '|anvil|cooking_range|fire|furnace|pottery_oven|pottery_wheel|spinning_wheel|workbench'],
['tool', 'Tool', 'select', '|pickaxe|axe|fire|chisel|hammer|knife|needle|rake|saw|shears|spade|watering_can|fishing_rod|fly_rod|harpoon|small_net|big_net|lobster_pot'],
@@ -65,8 +65,9 @@ var SECTIONS = [
['end_message', 'End Message', 'text', 'e.g. You\'ve finished smithing.', 'wide'],
],
fieldGroups: [
- ['type', 'subtype', 'level', 'xp', 'wait', 'output_qty'],
- {fields: ['station', 'tool'], right: true},
+ ['type', 'subtype', 'level', 'xp', 'ticks_per_cycle', 'output_qty'],
+ {note: 'Output Qty is the quantity produced per cycle, only relevant for stackable items.'},
+ ['station', 'tool'],
{label: 'Success', fields: ['success_base', 'success_per_level', 'success_cap', 'fail']},
['start_message', 'success_message', 'fail_message', 'end_message'],
],
@@ -213,6 +214,7 @@ function renderItemEditor(id, data) {
$('#editorMain').innerHTML = html;
setupSearchFields();
setupCraftTypeChange();
+ updateOutputQtyVisibility();
setTimeout(function() {
document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); });
}, 50);
@@ -229,7 +231,7 @@ function renderCurrent() {
function renderCard(sec, data) {
var collapsed = expandedCards[sec.id] === false;
- var h = '<div class="obj-card' + (sec.id === 'crafting' ? ' obj-card-full' : '') + '" data-card="' + sec.id + '">';
+ var h = '<div class="obj-card" data-card="' + sec.id + '">';
h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">';
h += '<span class="obj-card-arrow">' + (collapsed ? '&#9654;' : '&#9660;') + '</span>';
h += '<span class="obj-card-label">' + sec.label + '</span>';
@@ -600,6 +602,10 @@ function renderSubtable(sec, st, data) {
function renderGroup(fieldKeys) {
fieldKeys.forEach(function(fieldKey) {
+ if (fieldKey === '|') {
+ h += '<span class="craft-sep" style="color:#555;margin:0 4px;font-size:14px;line-height:1;align-self:center">|</span>';
+ return;
+ }
var f = st.fields.find(function(sf) { return sf[0] === fieldKey; });
if (!f) return;
if (f[5] && !f[5](item)) return;
@@ -610,9 +616,11 @@ function renderSubtable(sec, st, data) {
if (st.fieldGroups) {
st.fieldGroups.forEach(function(group) {
- if (group.label) {
+ if (group.note) {
+ h += '<div class="obj-field-note output-qty-note" style="margin:2px 0;padding:2px 8px;font-size:11px;color:#888;font-style:italic;line-height:1.3">' + esc(group.note) + '</div>';
+ } else if (group.label) {
if (group.label === 'Success') {
- var hasSuccess = !!(item.fail || item.success_base || item.success_per_level || item.success_cap);
+ var hasSuccess = !!(item.fail || (item.success && (item.success.base || item.success.per_level || item.success.cap)) || item.success_base || item.success_per_level || item.success_cap);
var gid = 'sg_' + sec.id + '_' + st.key + '_' + idx;
h += '<div class="obj-inline-group success-group" id="' + gid + '" style="margin:2px 0;padding:4px 8px' + (hasSuccess ? '' : ';display:none') + '">';
h += '<div style="display:flex;align-items:center;margin-bottom:2px">';
@@ -1039,7 +1047,7 @@ function setupCraftTypeChange() {
var xpField = document.getElementById(id.replace(/_type$/, '_xp'));
var stationField = document.getElementById(id.replace(/_type$/, '_station'));
- var combineHideFields = ['success_message', 'fail_message', 'tool', 'wait',
+ var combineHideFields = ['success_message', 'fail_message', 'tool', 'ticks_per_cycle',
'success_base', 'success_per_level', 'success_cap', 'fail'];
var recipeRow = typeSelect.closest('.obj-sub-row');
@@ -1065,6 +1073,9 @@ function setupCraftTypeChange() {
if (lbl) lbl.style.display = t === 'combine' ? 'none' : '';
});
+ var sepEl = recipeRow.querySelector('.craft-sep');
+ if (sepEl) sepEl.style.display = t === 'combine' ? 'none' : '';
+
var successGroup = document.querySelector('.success-group');
if (successGroup) successGroup.style.display = t === 'combine' ? 'none' : '';
var successBtn = document.getElementById(successGroup ? successGroup.id + '_btn' : '');
@@ -1081,10 +1092,10 @@ function setupCraftTypeChange() {
var sLbl = subtypeSel.closest('.obj-field');
if (t === 'smithing') {
if (sLbl) sLbl.style.display = '';
- subtypeSel.innerHTML = '<option value="">none</option><option value="smelt">smelt</option>';
+ subtypeSel.innerHTML = '<option value="">none</option><option value="smelting">smelting</option>';
} else if (t === 'pharmacy') {
if (sLbl) sLbl.style.display = '';
- subtypeSel.innerHTML = '<option value="">none</option><option value="clean">clean</option>';
+ subtypeSel.innerHTML = '<option value="">none</option><option value="cleaning">cleaning</option>';
} else {
if (sLbl) sLbl.style.display = 'none';
subtypeSel.value = '';
@@ -1097,6 +1108,26 @@ function setupCraftTypeChange() {
});
}
+function updateOutputQtyVisibility() {
+ var isStackable = false;
+ var stackableEl = document.getElementById('f_core_stackable');
+ if (stackableEl) {
+ isStackable = stackableEl.checked;
+ if (!stackableEl._hasOutputQtyListener) {
+ stackableEl._hasOutputQtyListener = true;
+ stackableEl.addEventListener('change', updateOutputQtyVisibility);
+ }
+ }
+
+ document.querySelectorAll('[id$="_output_qty"]').forEach(function(el) {
+ var lbl = el.closest('.obj-field');
+ if (lbl) lbl.style.display = isStackable ? '' : 'none';
+ });
+ document.querySelectorAll('.output-qty-note').forEach(function(el) {
+ el.style.display = isStackable ? '' : 'none';
+ });
+}
+
function setupSearchFields() {
document.querySelectorAll('.search-input').forEach(function(input) {
if (input._searchSetup) return;
@@ -1109,7 +1140,7 @@ function setupSearchFields() {
input.addEventListener('input', function() {
var val = input.value.trim();
- if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; }
+ if (!val) { results.style.display = 'none'; results.innerHTML = ''; results._selectedIndex = -1; return; }
API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) {
var items = [];
if (entityType === 'items' && data.items) items = data.items;
@@ -1126,6 +1157,7 @@ function setupSearchFields() {
return '<div class="sr-item" onclick="event.stopPropagation();selectSearchResult(this)">' + esc(r.id) + ' <span style="color:#aaa">' + esc(r.name || '') + '</span></div>';
}).join('');
results._items = items;
+ results._selectedIndex = -1;
}).catch(function() {
results.style.display = 'none';
});
@@ -1140,6 +1172,29 @@ function setupSearchFields() {
input.addEventListener('blur', function() {
setTimeout(function() { results.style.display = 'none'; }, 200);
});
+
+ input.addEventListener('keydown', function(e) {
+ if (results.style.display === 'none' || !results.children.length) return;
+ if (results._selectedIndex === undefined) results._selectedIndex = -1;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ results._selectedIndex = Math.min(results._selectedIndex + 1, results.children.length - 1);
+ updateSearchHighlight(results);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ results._selectedIndex = Math.max(results._selectedIndex - 1, 0);
+ updateSearchHighlight(results);
+ } else if (e.key === 'Enter') {
+ if (results._selectedIndex >= 0) {
+ e.preventDefault();
+ selectSearchResult(results.children[results._selectedIndex]);
+ }
+ } else if (e.key === 'Escape') {
+ results.style.display = 'none';
+ results._selectedIndex = -1;
+ }
+ });
});
document.addEventListener('click', function(e) {
@@ -1160,6 +1215,13 @@ function selectSearchResult(el) {
}
}
+function updateSearchHighlight(results) {
+ for (var i = 0; i < results.children.length; i++) {
+ results.children[i].classList.toggle('active', i === results._selectedIndex);
+ if (i === results._selectedIndex) results.children[i].scrollIntoView({block: 'nearest'});
+ }
+}
+
// =========================================================================
// Validation
// =========================================================================
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index e5d9a19..95bde7e 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -594,8 +594,8 @@ function renderItemsTab(spawns) {
spawns.forEach(function(spawn, i) {
h += '<div class="entry-row" data-idx="' + i + '">';
h += '<div class="entry-fields">';
- h += '<span class="entry-id">' + esc(spawn.id || '') + '</span>';
h += '<div style="display:flex;gap:8px;align-items:center;">';
+ h += '<span class="entry-id" style="flex:1;display:inline;padding:0">' + esc(spawn.id || '') + '</span>';
h += '<div><label class="mini-label">Quantity</label><input class="mini-input spawn-qty" type="text" inputmode="numeric" pattern="[0-9]*" value="' + (spawn.quantity || 0) + '" onchange="updateSpawn(' + i + ')"></div>';
h += '<div><label class="mini-label">Respawn</label><input class="mini-input spawn-respawn" type="text" inputmode="numeric" pattern="[0-9]*" value="' + (spawn.respawn_ticks || 0) + '" onchange="updateSpawn(' + i + ')"></div>';
h += '</div>';
@@ -1066,7 +1066,7 @@ function searchMobs(input) {
function searchAndShow(input, type, onSelect) {
var val = input.value.trim();
var results = input.parentElement.querySelector('.search-results');
- if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; }
+ if (!val) { results.style.display = 'none'; results.innerHTML = ''; results._selectedIndex = -1; return; }
API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) {
var items = [];
if (type === 'objects' && data.objects) items = data.objects;
@@ -1084,9 +1084,36 @@ function searchAndShow(input, type, onSelect) {
}).join('');
results._items = items;
results._onSelect = onSelect;
+ results._selectedIndex = -1;
}).catch(function() {
results.style.display = 'none';
});
+
+ input._searchKeySetup = input._searchKeySetup || (function() {
+ input.addEventListener('keydown', function(e) {
+ if (results.style.display === 'none' || !results.children.length) return;
+ if (results._selectedIndex === undefined) results._selectedIndex = -1;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ results._selectedIndex = Math.min(results._selectedIndex + 1, results.children.length - 1);
+ updateSearchHighlight(results);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ results._selectedIndex = Math.max(results._selectedIndex - 1, 0);
+ updateSearchHighlight(results);
+ } else if (e.key === 'Enter') {
+ if (results._selectedIndex >= 0) {
+ e.preventDefault();
+ selectSearchResult(results.children[results._selectedIndex]);
+ }
+ } else if (e.key === 'Escape') {
+ results.style.display = 'none';
+ results._selectedIndex = -1;
+ }
+ });
+ return true;
+ })();
}
function selectSearchResult(el) {
@@ -1101,6 +1128,13 @@ function selectSearchResult(el) {
}
}
+function updateSearchHighlight(results) {
+ for (var i = 0; i < results.children.length; i++) {
+ results.children[i].classList.toggle('active', i === results._selectedIndex);
+ if (i === results._selectedIndex) results.children[i].scrollIntoView({block: 'nearest'});
+ }
+}
+
function openEditor(type, id) {
window.open('/editor/' + type + '#' + id, '_blank');
}
@@ -1111,11 +1145,13 @@ function reRenderPanel() {
}
function autoSave() {
+ if (window._colorPickerActive) return;
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(function() { doSavePanel(); }, 600);
}
function saveNow() {
+ if (window._colorPickerActive) return;
if (saveTimer) clearTimeout(saveTimer);
saveTimer = null;
doSavePanel();
diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js
index 13a983e..56894d2 100644
--- a/internal/admin/static/objecteditor.js
+++ b/internal/admin/static/objecteditor.js
@@ -103,7 +103,7 @@ var SECTIONS = [
},
{
id: 'use_interactions', label: 'Use Interactions', path: 'use_interactions', isArray: true,
- detect: function(d) { return d.use_interactions && d.use_interactions.length; },
+ detect: function(d) { return d.use_interactions && Array.isArray(d.use_interactions); },
fields: [
['item_id', 'Item ID', 'text'],
['message', 'Message', 'text'],
@@ -150,6 +150,7 @@ window.onTalkTreeChange = function(data) {
function loadObject(id) {
objectID = id;
+ currentID = id;
window.location.hash = id;
renderList('');
API.get('/api/objects/' + encodeURIComponent(id)).then(function(data) {
@@ -577,6 +578,12 @@ function addSection() {
} else {
objectData[sec.path] = {};
}
+ } else if (id === 'steal') {
+ objectData.steal_table = '';
+ objectData.steal_level = 0;
+ objectData.steal_xp = 0;
+ objectData.steal_speed = 0;
+ objectData.guard_mob = '';
}
expandedCards[id] = true;
renderCurrent();
@@ -683,7 +690,7 @@ function setupSearchFields() {
input.addEventListener('input', function() {
var val = input.value.trim();
- if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; }
+ if (!val) { results.style.display = 'none'; results.innerHTML = ''; results._selectedIndex = -1; return; }
API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) {
var items = [];
if (entityType === 'items' && data.items) items = data.items;
@@ -700,6 +707,7 @@ function setupSearchFields() {
return '<div class="sr-item" onclick="event.stopPropagation();selectObjSearchResult(this)">' + esc(r.id) + ' <span style="color:#aaa">' + esc(r.name || '') + '</span></div>';
}).join('');
results._items = items;
+ results._selectedIndex = -1;
}).catch(function() {
results.style.display = 'none';
});
@@ -714,6 +722,29 @@ function setupSearchFields() {
input.addEventListener('blur', function() {
setTimeout(function() { results.style.display = 'none'; }, 200);
});
+
+ input.addEventListener('keydown', function(e) {
+ if (results.style.display === 'none' || !results.children.length) return;
+ if (results._selectedIndex === undefined) results._selectedIndex = -1;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ results._selectedIndex = Math.min(results._selectedIndex + 1, results.children.length - 1);
+ updateSearchHighlight(results);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ results._selectedIndex = Math.max(results._selectedIndex - 1, 0);
+ updateSearchHighlight(results);
+ } else if (e.key === 'Enter') {
+ if (results._selectedIndex >= 0) {
+ e.preventDefault();
+ selectObjSearchResult(results.children[results._selectedIndex]);
+ }
+ } else if (e.key === 'Escape') {
+ results.style.display = 'none';
+ results._selectedIndex = -1;
+ }
+ });
});
document.addEventListener('click', function(e) {
@@ -734,6 +765,13 @@ function selectObjSearchResult(el) {
}
}
+function updateSearchHighlight(results) {
+ for (var i = 0; i < results.children.length; i++) {
+ results.children[i].classList.toggle('active', i === results._selectedIndex);
+ if (i === results._selectedIndex) results.children[i].scrollIntoView({block: 'nearest'});
+ }
+}
+
function validateFields() {
var errors = [];
document.querySelectorAll('.obj-field-error').forEach(function(field) {
@@ -974,16 +1012,13 @@ function deleteObject() {
notify('Object ' + objectID + ' deleted', 'success');
updateUndoBar();
objectID = null;
+ currentID = null;
objectData = null;
$('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>';
loadList();
}).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
}
-// Re-exports for editor.js compatibility
-var currentID = null;
-Object.defineProperty(window, 'currentID', { get: function() { return objectID; }, set: function(v) { objectID = v; } });
-
function loadItem(id) { loadObject(id); }
function createNew() {
var name = prompt('Object ID:');