aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-12 21:16:58 -0400
committerhistoria <[not public]>2026-07-12 21:16:58 -0400
commitf601a40776843f6196221e74ba502c571a5b1bcd (patch)
treea5b3fc0ae5b8774d642439fc4d369b96bf3932ab /internal
parentbeb0b73a9e56fdc717d55bbf1a1c7c5b49d4c6a4 (diff)
downloadthehouseoficarus-f601a40776843f6196221e74ba502c571a5b1bcd.tar.gz
fix(admin): standardize most search dropdowns and radio buttons
Diffstat (limited to 'internal')
-rw-r--r--internal/admin/api_stations.go37
-rw-r--r--internal/admin/server.go1
-rw-r--r--internal/admin/static/admin.css29
-rw-r--r--internal/admin/static/cardeditor.js48
-rw-r--r--internal/admin/static/hazardeditor.js2
-rw-r--r--internal/admin/static/itemeditor.js23
-rw-r--r--internal/admin/static/mobeditor.js38
-rw-r--r--internal/admin/static/moduleeditor.js8
-rw-r--r--internal/admin/static/talktree.js8
-rw-r--r--internal/admin/static/techeditor.js4
-rw-r--r--internal/admin/templates/drops.html2
-rw-r--r--internal/admin/templates/files.html2
-rw-r--r--internal/admin/templates/hazards.html2
-rw-r--r--internal/admin/templates/items.html2
-rw-r--r--internal/admin/templates/mobs.html2
-rw-r--r--internal/admin/templates/modules.html2
-rw-r--r--internal/admin/templates/objects.html2
-rw-r--r--internal/admin/templates/players.html2
-rw-r--r--internal/admin/templates/techs.html2
-rw-r--r--internal/admin/templates/triggers.html2
20 files changed, 179 insertions, 39 deletions
diff --git a/internal/admin/api_stations.go b/internal/admin/api_stations.go
new file mode 100644
index 0000000..411ed96
--- /dev/null
+++ b/internal/admin/api_stations.go
@@ -0,0 +1,37 @@
+package admin
+
+import (
+ "net/http"
+)
+
+func (s *AdminServer) handleStations(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ items, err := s.itemStore.LoadAll()
+ if err != nil {
+ http.Error(w, "failed to load items", http.StatusInternalServerError)
+ return
+ }
+
+ seen := map[string]bool{}
+ var stations []string
+ for _, def := range items {
+ if def.Craft != nil {
+ for _, c := range def.Craft {
+ for _, s := range c.Station {
+ if s != "" {
+ if !seen[s] {
+ seen[s] = true
+ stations = append(stations, s)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ writeJSON(w, stations)
+}
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 11efb5c..cc63cee 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -171,6 +171,7 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor
apiMux.HandleFunc("/api/dashboard", s.handleDashboard)
apiMux.HandleFunc("/api/global_flags", s.handleGlobalFlags)
apiMux.HandleFunc("/api/tools", s.handleTools)
+ apiMux.HandleFunc("/api/stations", s.handleStations)
apiMux.HandleFunc("/api/search", s.handleSearch)
apiMux.HandleFunc("/api/undo/state", s.handleUndoState)
apiMux.HandleFunc("/api/undo/undo", s.doUndo)
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index c500aad..a1a364e 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -38,7 +38,8 @@ body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;f
.table a{color:var(--text);text-decoration:none}
.table a:hover{color:#fff}
.search-bar{margin-bottom:12px}
-.search-bar input{width:100%;padding:6px 8px;font-size:13px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px}
+.search-bar input{width:100%;padding:6px 28px 6px 8px;font-size:13px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;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 8px center}
+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}
.login{max-width:360px;margin:80px auto;background:var(--panel);padding:30px;border:1px solid var(--border);border-radius:6px}
.login h1{font-size:18px;margin-bottom:20px;text-align:center}
.z-controls-wrapper{position:absolute;top:12px;left:12px;z-index:10}
@@ -278,7 +279,7 @@ g.ud-hover:hover text{font-weight:bold}
.panel-tab-content .entry-fields textarea{resize:vertical;min-height:28px}
.panel-tab-content .entry-fields .entry-id{font-size:11px;color:#6cf;display:block;padding:3px 0}
.panel-tab-content .section-search{position:relative;margin-top:4px}
-.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-input{width:100%;padding:4px 24px 4px 6px;font-size:11px;background-color: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,.panel-tab-content .search-results .sr-item.active{background:var(--accent)}
@@ -300,7 +301,7 @@ g.ud-hover:hover text{font-weight:bold}
.obj-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px 8px}
.obj-field{display:flex;flex-direction:column;gap:2px;font-size:11px}
.obj-field span{color:#888;font-size:9px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.obj-field input,.obj-field textarea,.obj-field select{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;width:100%}
+.obj-field input,.obj-field textarea,.obj-field select{padding:4px 6px;font-size:11px;background-color:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace;width:100%}
.obj-field textarea{resize:vertical;min-height:28px}
.obj-field.obj-wide{grid-column:1/-1}
.obj-field.obj-check{flex-direction:row;align-items:center;gap:4px;padding-bottom:3px}
@@ -313,6 +314,15 @@ g.ud-hover:hover text{font-weight:bold}
.seg-toggle input:checked+span{color:#3a8fd4}
.seg-toggle label:has(input:checked){color:var(--text);background:var(--accent)}
.seg-toggle input{position:absolute;opacity:0;pointer-events:none;width:0;height:0}
+.seg-pill{position:relative;border-radius:6px;background:var(--input-bg);display:inline-flex;border:1px solid var(--border)}
+.seg-pill .seg-opt{flex:1;text-align:center;position:relative;z-index:1;padding:5px 14px;font-size:11px;cursor:pointer;color:#888;background:transparent;border:none;font-family:monospace;transition:color .15s}
+.seg-pill .seg-opt:hover{color:var(--text)}
+.seg-pill .seg-opt.seg-active{color:var(--text)}
+.seg-pill::before{content:'';position:absolute;top:2px;bottom:2px;left:0;width:calc(100% / 3);background:var(--accent);border-radius:4px;transition:transform .2s ease;z-index:0}
+.seg-pill.seg-pill-3.seg-idx-0::before{transform:translateX(0)}
+.seg-pill.seg-pill-3.seg-idx-1::before{transform:translateX(100%)}
+.seg-pill.seg-pill-3.seg-idx-2::before{transform:translateX(200%)}
+.seg-pill .seg-opt:focus-visible{outline:2px solid var(--accent);outline-offset:-1px;border-radius:3px;z-index:2}
.obj-color-row{display:flex;align-items:center;gap:4px}
.obj-color-row .color-swatch{flex-shrink:0}
.obj-color-row input{width:38px!important;text-align:center;padding:4px 2px}
@@ -335,16 +345,21 @@ g.ud-hover:hover text{font-weight:bold}
.add-section-bar button{padding:4px 12px;font-size:11px}
.obj-kv-list{margin-top:4px}
.obj-kv-row{display:flex;gap:4px;align-items:center;margin-bottom:3px}
-.obj-kv-row input{padding:3px 5px;font-size:10px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:2px;font-family:monospace;flex:1}
+.obj-kv-row input{padding:3px 5px;font-size:10px;background-color:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:2px;font-family:monospace;flex:1}
.obj-kv-row .obj-kv-key{flex:1}
+.obj-kv-key-wrap.obj-search{position:relative!important}
+.obj-kv-key-wrap.obj-search .search-input.obj-kv-key{width:100%;flex:none;padding-right:24px}
+.obj-kv-key-wrap.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-kv-key-wrap.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-kv-key-wrap.obj-search .search-results .sr-item:hover,.obj-kv-key-wrap.obj-search .search-results .sr-item.active{background:var(--accent)}
.obj-kv-row .obj-kv-val{width:70px;flex:none}
.obj-core-row{display:flex;gap:6px;align-items:flex-end;margin-bottom:8px}
-.obj-field-error input,.obj-field-error textarea,.obj-field-error select{border-color:var(--danger)!important;background:rgba(192,57,43,.08)}
+.obj-field-error input,.obj-field-error textarea,.obj-field-error select{border-color:var(--danger)!important;background-color:rgba(192,57,43,.08)}
.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,.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{padding-right:24px}
.obj-search input.search-input::placeholder{color:#666}
.obj-narrow.obj-search input{width:80px}
.obj-sub-row .obj-search input.search-input{min-width:80px}
@@ -475,3 +490,5 @@ g.ud-hover:hover text{font-weight:bold}
.obj-card[data-card="trigger_action"]>.obj-card-body>.obj-sub-add{display:none}
.obj-card[data-card="trigger_action"] .ie-inter-header .ie-trig-up,
.obj-card[data-card="trigger_action"] .ie-inter-header .ie-trig-dn{display:none}
+input:focus-visible,select:focus-visible,textarea:focus-visible,button:focus-visible{outline:2px solid rgba(100,160,255,.5);outline-offset:1px}
+.sr-no-results{padding:6px 8px;font-size:11px;color:#666;text-align:center;cursor:default;pointer-events:none}
diff --git a/internal/admin/static/cardeditor.js b/internal/admin/static/cardeditor.js
index 1e8d259..1b2b76b 100644
--- a/internal/admin/static/cardeditor.js
+++ b/internal/admin/static/cardeditor.js
@@ -90,6 +90,46 @@ function ced_addKVRow(cardID, subKey) {
el.appendChild(row);
}
+function ced_buildDynamicSelect(fid, dpAttr, val, dynKey) {
+ var items = (window._dynOpts && window._dynOpts[dynKey]) ? window._dynOpts[dynKey].slice().sort() : [];
+ var s = '<select id="' + fid + '"' + dpAttr + '>';
+ s += '<option value="">(none)</option>';
+ items.forEach(function(o) {
+ s += '<option value="' + escAttr(o) + '"' + (String(val) === o ? ' selected' : '') + '>' + esc(o) + '</option>';
+ });
+ s += '</select>';
+ return s;
+}
+
+function ced_renderKVSearch(sec, il, data, getter) {
+ var sectionRoot = data[ced_cardPath(sec)] || data;
+ var map = getter ? getter(sectionRoot, il) : (sectionRoot[il.key] || {});
+ var entity = il.searchEntity || 'items';
+ var h = '<div class="obj-inline-group">';
+ h += '<span class="obj-inline-label">' + esc(il.label) + '</span>';
+ h += '<div class="obj-kv-list" data-sec="' + sec.id + '" data-key="' + il.key + '">';
+ Object.keys(map).forEach(function(k, idx) {
+ h += '<div class="obj-kv-row">';
+ h += '<div class="obj-kv-key-wrap obj-search" style="flex:1;position:relative"><div style="position:relative;width:100%"><input value="' + escAttr(k) + '" class="search-input obj-kv-key" data-search-type="' + entity + '" placeholder="Search ' + entity + '..."><div class="search-results" style="display:none"></div></div></div>';
+ h += '<input value="' + escAttr(String(map[k])) + '" placeholder="val" class="obj-kv-val">';
+ h += '<button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button></div>';
+ });
+ h += '</div>';
+ h += '<button class="btn btn-sm obj-sub-add" onclick="ced_addKVSearchRow(\'' + sec.id + '\',\'' + il.key + '\',\'' + entity + '\')">+ Add</button>';
+ h += '</div>';
+ return h;
+}
+
+function ced_addKVSearchRow(cardID, subKey, entity) {
+ var el = document.querySelector('.obj-kv-list[data-sec="' + cardID + '"][data-key="' + subKey + '"]');
+ if (!el) return;
+ var row = document.createElement('div');
+ row.className = 'obj-kv-row';
+ row.innerHTML = '<div class="obj-kv-key-wrap obj-search" style="flex:1;position:relative"><div style="position:relative;width:100%"><input value="" class="search-input obj-kv-key" data-search-type="' + entity + '" placeholder="Search ' + entity + '..."><div class="search-results" style="display:none"></div></div></div><input value="0" placeholder="val" class="obj-kv-val"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button>';
+ el.appendChild(row);
+ ced_setupSearchFields(row);
+}
+
// ---- Search autocomplete ----
function ced_setupSearchFields(root) {
@@ -114,7 +154,7 @@ function ced_setupSearchFields(root) {
if (entityType === 'drops' && data.drops) items = data.drops;
if (items.length === 0) {
results.style.display = 'block';
- results.innerHTML = '<div class="sr-item" style="color:#888">No results</div>';
+ results.innerHTML = '<div class="sr-item sr-no-results">No results</div>';
return;
}
results.style.display = 'block';
@@ -315,6 +355,10 @@ function ced_renderField(sec, f, data, idx, subPath, optStyle, cardPathFn, field
return wrap('obj-field', '<span>' + esc(label) + '</span><textarea id="' + fid + '" rows="3"' + dpAttr + '>' + esc(dstr) + '</textarea>');
}
if (type === 'select') {
+ if (hint && hint.indexOf('dyn:') === 0) {
+ var inner = '<span>' + esc(label) + '</span>' + ced_buildDynamicSelect(fid, dpAttr, val, hint.substring(4));
+ return wrap('obj-field', inner);
+ }
var opts = hint ? hint.split('|') : [];
var inner = '<span>' + esc(label) + '</span><select id="' + fid + '"' + dpAttr + '>';
opts.forEach(function(o) {
@@ -423,7 +467,7 @@ function ced_saveInlineFields(sec, sectionObj, cardPathFn, fieldIDFn) {
sectionObj[il.key] = el.value.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; });
hasValues = true;
}
- } else if (il.type === 'kv') {
+ } else if (il.type === 'kv' || il.type === 'kv_search') {
var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]');
if (kvEl) {
var map = {};
diff --git a/internal/admin/static/hazardeditor.js b/internal/admin/static/hazardeditor.js
index 10def4b..e849512 100644
--- a/internal/admin/static/hazardeditor.js
+++ b/internal/admin/static/hazardeditor.js
@@ -13,7 +13,7 @@ var HAZARD_SECTIONS = [
['warn_message', 'Warn Message', 'text', 'The air grows thick with toxic fumes...', 'wide'],
['hit_message', 'Hit Message', 'text', 'You cough and choke on the toxic gas!'],
['miss_message', 'Miss Message', 'text', 'You hold your breath and avoid the fumes.'],
- ['required_item', 'Required Item', 'text', 'e.g. gas_mask'],
+ ['required_item', 'Required Item', 'search', 'e.g. gas_mask', '', null, 'items'],
['speed', 'Speed', 'number', '10', 'narrow'],
]
},
diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js
index 53242ab..d48d70e 100644
--- a/internal/admin/static/itemeditor.js
+++ b/internal/admin/static/itemeditor.js
@@ -53,8 +53,8 @@ var SECTIONS = [
['xp', 'XP', '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'],
+ ['station', 'Station', 'select', 'dyn:stations'],
+ ['tool', 'Tool', 'select', 'dyn:tools'],
['fail', 'Fail Item', 'search', 'e.g. burnt_fish', '', null, 'items'],
['success_base', 'Base', 'number', '0.5', 'narrow'],
['success_per_level', 'Per Lvl', 'number', '0.01', 'narrow'],
@@ -125,6 +125,7 @@ function initItemEditor() {
window._ieRenderCurrent = renderCurrent;
window._ieRenderOnly = function() { if (itemID && itemData) renderItemEditor(itemID, itemData); };
window._ieSyncAll = function(ed) { if (ed) ie_syncAllInteractions(ed, SECTIONS); };
+ fetchDynamicOptions();
editorType = 'items';
editorFields = [];
loadList();
@@ -647,7 +648,11 @@ function renderSubFieldHTML(sec, stKey, idx, f, val) {
return '<label class="obj-field obj-check' + extraCls + '"' + extraStyle + '><input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + dpAttr + '> <span>' + esc(f[1]) + '</span></label>';
}
if (f[2] === 'select') {
- var opts = f[3] ? f[3].split('|') : [];
+ var hint = f[3] || '';
+ if (hint.indexOf('dyn:') === 0) {
+ return '<label class="obj-field' + extraCls + '"' + extraStyle + '><span>' + esc(f[1]) + '</span>' + ced_buildDynamicSelect(fid, dpAttr, val, hint.substring(4)) + '</label>';
+ }
+ var opts = hint.split('|');
var sel = '<label class="obj-field' + extraCls + '"' + extraStyle + '><span>' + esc(f[1]) + '</span><select id="' + fid + '"' + dpAttr + '>';
opts.forEach(function(o) {
sel += '<option value="' + escAttr(o) + '"' + (String(val) === o ? ' selected' : '') + '>' + esc(o || '(none)') + '</option>';
@@ -1244,6 +1249,18 @@ function selectSearchResult(el) { ced_selectSearchResult(el); }
function updateSearchHighlight(results) { ced_updateSearchHighlight(results); }
+function fetchDynamicOptions() {
+ Promise.all([
+ API.get('/api/tools').catch(function() { return []; }),
+ API.get('/api/stations').catch(function() { return []; })
+ ]).then(function(results) {
+ window._dynOpts = window._dynOpts || {};
+ window._dynOpts.tools = results[0] || [];
+ window._dynOpts.stations = results[1] || [];
+ if (itemID && itemData) renderCurrent();
+ });
+}
+
// =========================================================================
// Validation
// =========================================================================
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js
index 2c8db21..3934fcd 100644
--- a/internal/admin/static/mobeditor.js
+++ b/internal/admin/static/mobeditor.js
@@ -46,7 +46,7 @@ var MOB_SECTIONS = [
['stats.defenses.crush_defense', 'Crush Def', 'number', '0', 'narrow'],
['stats.defenses.science_defense', 'Sci Def', 'number', '0', 'narrow'],
['stats.defenses.ranged_defense', 'Rng Def', 'number', '0', 'narrow'],
- ['stats.defenses.weakness', 'Weakness', 'text', 'e.g. crush'],
+ ['stats.defenses.weakness', 'Weakness', 'select', '|bio|eco|hydro|solar|chaos'],
['stats.defenses.weakness_percent', 'Weakness %', 'number', '50', 'narrow'],
['assassin_level', 'Assassin Lvl', 'number', '1', 'narrow'],
['finishing_blow', 'Finishing Blow', 'search', 'e.g. stake', '', null, 'items'],
@@ -314,19 +314,22 @@ function normalizeAttackTypes(v) {
}
// renderMobAttackTypeWidget renders the Atk Type selector: one required melee
-// type (radio: stab/slash/crush) plus optional ranged/science (checkboxes).
+// type (pill: stab/slash/crush) plus optional ranged/science (checkboxes).
function renderMobAttackTypeWidget(data) {
var combat = data.combat || {};
var types = normalizeAttackTypes(combat.attack_types);
var melee = types.filter(function(t) { return t === 'stab' || t === 'slash' || t === 'crush'; })[0] || 'crush';
var hasRanged = types.indexOf('ranged') >= 0;
var hasScience = types.indexOf('science') >= 0;
+ var meleeOpts = ['stab','slash','crush'];
+ var activeIdx = meleeOpts.indexOf(melee);
+ if (activeIdx < 0) activeIdx = 2;
var h = '<div class="obj-inline-group">';
h += '<span class="obj-inline-label">Atk Type</span>';
- h += '<div class="obj-card-grid" style="margin-top:4px">';
- ['stab','slash','crush'].forEach(function(m) {
- h += '<label class="obj-field obj-check obj-narrow"><input type="radio" name="mob_atktype_melee" value="' + m + '"' + (melee === m ? ' checked' : '') + '> <span>' + m + '</span></label>';
+ h += '<div class="seg-pill seg-pill-3 seg-idx-' + activeIdx + '" id="mob_atktype_pill" style="margin-top:4px">';
+ meleeOpts.forEach(function(t, i) {
+ h += '<button type="button" class="seg-opt' + (i === activeIdx ? ' seg-active' : '') + '" data-v="' + t + '" onclick="mobAtkPillSelect(\'' + t + '\',this)">' + t + '</button>';
});
h += '</div>';
h += '<div class="obj-card-grid" style="margin-top:4px">';
@@ -337,12 +340,31 @@ function renderMobAttackTypeWidget(data) {
return h;
}
+function mobAtkPillSelect(val, btn) {
+ var pill = document.getElementById('mob_atktype_pill');
+ if (!pill) return;
+ var opts = pill.querySelectorAll('.seg-opt');
+ var idx = -1;
+ opts.forEach(function(o, i) {
+ if (o.getAttribute('data-v') === val) {
+ o.classList.add('seg-active');
+ idx = i;
+ } else {
+ o.classList.remove('seg-active');
+ }
+ });
+ pill.className = pill.className.replace(/seg-idx-\d/g, 'seg-idx-' + idx);
+}
+
// collectMobAttackType reads the Atk Type widget into an array: the selected
// melee type first, then any optional ranged/science types.
function collectMobAttackType() {
var melee = 'crush';
- var r = document.querySelector('input[name="mob_atktype_melee"]:checked');
- if (r) melee = r.value;
+ var pill = document.getElementById('mob_atktype_pill');
+ if (pill) {
+ var active = pill.querySelector('.seg-opt.seg-active');
+ if (active) melee = active.getAttribute('data-v');
+ }
var out = [melee];
var rng = document.getElementById('mob_atktype_ranged');
var sci = document.getElementById('mob_atktype_science');
@@ -852,7 +874,7 @@ function saveMob() {
hasValues = ced_saveSectionFields(sec, sectionObj, cardPath, fieldID) || hasValues;
}
- if (sec.id === 'combat' && document.querySelector('input[name="mob_atktype_melee"]')) {
+ if (sec.id === 'combat' && document.getElementById('mob_atktype_pill')) {
sectionObj['attack_types'] = collectMobAttackType();
hasValues = true;
}
diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js
index 739fc2e..fe71f88 100644
--- a/internal/admin/static/moduleeditor.js
+++ b/internal/admin/static/moduleeditor.js
@@ -13,11 +13,11 @@ var MODULE_SECTIONS = [
['max_hit', 'Max Hit', 'number', '10', 'narrow'],
['base_xp', 'Base XP', 'number', '50', 'narrow'],
['category', 'Category', 'select', 'combat|utility|enchant|processing|transport'],
- ['element', 'Element', 'text', 'e.g. fire'],
+ ['element', 'Element', 'select', '|bio|eco|hydro|solar|chaos'],
['destination', 'Destination', 'number', '5', 'narrow'],
],
inline: [
- {label:'Junk Cost', key:'junk_cost', type:'kv', valType:'number'},
+ {label:'Junk Cost', key:'junk_cost', type:'kv_search', searchEntity:'items', valType:'number'},
]
},
{
@@ -144,6 +144,8 @@ function renderCard(sec, data) {
sec.inline.forEach(function(il) {
if (il.type === 'kv') {
h += ced_renderKV(sec, il, data);
+ } else if (il.type === 'kv_search') {
+ h += ced_renderKVSearch(sec, il, data);
}
});
}
@@ -274,7 +276,7 @@ function saveModule() {
if (sec.inline) {
sec.inline.forEach(function(il) {
- if (il.type === 'kv') {
+ if (il.type === 'kv' || il.type === 'kv_search') {
var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]');
if (kvEl) {
var map = {};
diff --git a/internal/admin/static/talktree.js b/internal/admin/static/talktree.js
index b8c7106..e55653e 100644
--- a/internal/admin/static/talktree.js
+++ b/internal/admin/static/talktree.js
@@ -207,7 +207,7 @@
else if (type === 'number') { var n = parseFloat(el.value); val = isNaN(n) ? 0 : n; }
else { val = el.value; }
talkSyncPath(path, val);
- if (path.endsWith('.goto')) renderTalkTree(window._talkData);
+ if (path.endsWith('.goto') && e.type === 'change') renderTalkTree(window._talkData);
} else if (tag === 'SELECT') {
talkSyncPath(path, el.value);
}
@@ -492,7 +492,7 @@
label = label || 'Auto-advance to';
return '<div class="talk-goto-fallback">' +
'<span class="talk-prop-label">' + esc(label) + '</span>' +
- '<input class="talk-input talk-input-sm" data-tp="nodes.' + escAttr(nodeKey) + '.goto" list="talkNodeKeys" value="' + escAttr(node.goto || '') + '" placeholder="Fallback node...">' +
+ '<input class="talk-input talk-input-sm search-input" data-tp="nodes.' + escAttr(nodeKey) + '.goto" list="talkNodeKeys" value="' + escAttr(node.goto || '') + '" placeholder="Fallback node...">' +
'</div>';
}
@@ -530,7 +530,7 @@
html += '<div class="talk-opt-edit-row">';
html += '<span class="talk-seq-num">' + (idx + 1) + '</span>';
html += '<input class="talk-input talk-opt-text" data-tp="nodes.' + escAttr(nodeKey) + '.options.' + idx + '.text" value="' + escAttr(opt.text || '') + '" placeholder="Option text...">';
- html += '<input class="talk-input talk-opt-goto" data-tp="nodes.' + escAttr(nodeKey) + '.options.' + idx + '.goto" list="talkNodeKeys" value="' + escAttr(opt.goto || '') + '" placeholder="Goto...">';
+ html += '<input class="talk-input talk-opt-goto search-input" data-tp="nodes.' + escAttr(nodeKey) + '.options.' + idx + '.goto" list="talkNodeKeys" value="' + escAttr(opt.goto || '') + '" placeholder="Goto...">';
html += '<div class="talk-opt-edit-actions">';
if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) {
@@ -582,7 +582,7 @@
html += '<div class="talk-add-opt-row">' +
'<span class="talk-seq-num" style="color:#555">' + (options.length + 1) + '</span>' +
'<input class="talk-input talk-opt-text" id="talkOptText_' + escAttr(nodeKey) + '" placeholder="Option text...">' +
- '<input class="talk-input talk-opt-goto" id="talkOptGoto_' + escAttr(nodeKey) + '" list="talkNodeKeys" placeholder="Goto...">' +
+ '<input class="talk-input talk-opt-goto search-input" id="talkOptGoto_' + escAttr(nodeKey) + '" list="talkNodeKeys" placeholder="Goto...">' +
'<span class="talk-opt-gap"></span>' +
'<button class="talk-add-btn" data-action="add-option" data-nk="' + escAttr(nodeKey) + '">Add</button>' +
'</div>';
diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js
index c9d7c24..812cded 100644
--- a/internal/admin/static/techeditor.js
+++ b/internal/admin/static/techeditor.js
@@ -10,8 +10,8 @@ var TECH_SECTIONS = [
['name', 'Name', 'text', 'Protect from Melee'],
['level', 'Level', 'number', '37', 'narrow'],
['drain_rate', 'Drain Rate', 'number', '0.5', 'narrow'],
- ['category', 'Category', 'text', 'protection'],
- ['group', 'Group', 'text', 'melee'],
+ ['category', 'Category', 'select', '|accuracy|combo|defense|protection|ranged|science|strength|utility'],
+ ['group', 'Group', 'select', '|accuracy_tier|defense_tier|protection|ranged_tier|regen_tier|science_tier|strength_tier'],
]
},
{
diff --git a/internal/admin/templates/drops.html b/internal/admin/templates/drops.html
index fb67c86..f270fe8 100644
--- a/internal/admin/templates/drops.html
+++ b/internal/admin/templates/drops.html
@@ -1,7 +1,7 @@
{{define "body-drops"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search drop tables..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search drop tables..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Drop Table</button>
</div>
diff --git a/internal/admin/templates/files.html b/internal/admin/templates/files.html
index 5a5106f..c9e0003 100644
--- a/internal/admin/templates/files.html
+++ b/internal/admin/templates/files.html
@@ -1,7 +1,7 @@
{{define "body-files"}}
<div class="editor-container">
<div class="editor-list" id="fileList">
- <div class="search-bar"><input placeholder="Filter files..." oninput="filterFiles(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search files..." oninput="filterFiles(this.value)"></div>
<div id="listEntries"></div>
</div>
<div class="editor-main" id="editorMain">
diff --git a/internal/admin/templates/hazards.html b/internal/admin/templates/hazards.html
index d7d4548..102165a 100644
--- a/internal/admin/templates/hazards.html
+++ b/internal/admin/templates/hazards.html
@@ -1,7 +1,7 @@
{{define "body-hazards"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search hazards..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search hazards..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Hazard</button>
</div>
diff --git a/internal/admin/templates/items.html b/internal/admin/templates/items.html
index 7efeded..f1c8ee5 100644
--- a/internal/admin/templates/items.html
+++ b/internal/admin/templates/items.html
@@ -1,7 +1,7 @@
{{define "body-items"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search items..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search items..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Item</button>
</div>
diff --git a/internal/admin/templates/mobs.html b/internal/admin/templates/mobs.html
index f53b3e3..14ac360 100644
--- a/internal/admin/templates/mobs.html
+++ b/internal/admin/templates/mobs.html
@@ -1,7 +1,7 @@
{{define "body-mobs"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search mobs..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search mobs..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Mob</button>
</div>
diff --git a/internal/admin/templates/modules.html b/internal/admin/templates/modules.html
index e46af1b..09c75a9 100644
--- a/internal/admin/templates/modules.html
+++ b/internal/admin/templates/modules.html
@@ -1,7 +1,7 @@
{{define "body-modules"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search modules..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search modules..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Module</button>
</div>
diff --git a/internal/admin/templates/objects.html b/internal/admin/templates/objects.html
index 9be1b66..3062c70 100644
--- a/internal/admin/templates/objects.html
+++ b/internal/admin/templates/objects.html
@@ -1,7 +1,7 @@
{{define "body-objects"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search objects..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search objects..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Object</button>
</div>
diff --git a/internal/admin/templates/players.html b/internal/admin/templates/players.html
index 43627d8..22ffc47 100644
--- a/internal/admin/templates/players.html
+++ b/internal/admin/templates/players.html
@@ -1,7 +1,7 @@
{{define "body-players"}}
<div style="padding:20px">
<h2>Players</h2>
- <div class="search-bar" style="max-width:400px"><input placeholder="Search players..." id="playerSearch" oninput="searchPlayers()"></div>
+ <div class="search-bar" style="max-width:400px"><input type="search" placeholder="Search players..." id="playerSearch" oninput="searchPlayers()"></div>
<table class="table" style="margin-top:12px">
<thead><tr><th>Name</th><th>Level</th><th>Room</th><th>Account</th></tr></thead>
<tbody id="playerTableBody"></tbody>
diff --git a/internal/admin/templates/techs.html b/internal/admin/templates/techs.html
index 813d57b..b4b678a 100644
--- a/internal/admin/templates/techs.html
+++ b/internal/admin/templates/techs.html
@@ -1,7 +1,7 @@
{{define "body-techs"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search techs..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search techs..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Tech</button>
</div>
diff --git a/internal/admin/templates/triggers.html b/internal/admin/templates/triggers.html
index 3b32765..d790427 100644
--- a/internal/admin/templates/triggers.html
+++ b/internal/admin/templates/triggers.html
@@ -1,7 +1,7 @@
{{define "body-triggers"}}
<div class="editor-container">
<div class="editor-list" id="itemList">
- <div class="search-bar"><input placeholder="Search triggers..." oninput="filterList(this.value)"></div>
+ <div class="search-bar"><input type="search" placeholder="Search triggers..." oninput="filterList(this.value)"></div>
<div id="listEntries"></div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%" onclick="createNew()">+ New Trigger</button>
</div>