From 1bedf06969b8ab8e51dd4301a0831200df5c8f59 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 13 Jul 2026 18:36:29 -0400 Subject: feat(admin): tooltips added for confusing fields, hide more optional fields with buttons --- internal/admin/static/cardeditor.js | 15 ++++--- internal/admin/static/itemeditor.js | 74 ++++++++++++++++++++++++++++------- internal/admin/static/mobeditor.js | 44 ++++++++++++++++++--- internal/admin/static/objecteditor.js | 44 ++++----------------- internal/admin/templates/items.html | 1 + 5 files changed, 115 insertions(+), 63 deletions(-) diff --git a/internal/admin/static/cardeditor.js b/internal/admin/static/cardeditor.js index 6a8cda7..cf3a8b7 100644 --- a/internal/admin/static/cardeditor.js +++ b/internal/admin/static/cardeditor.js @@ -301,7 +301,7 @@ function ced_updateSearchHighlight(results) { // ---- Generic field renderer (used by all card-based editors) ---- function ced_renderField(sec, f, data, idx, subPath, optStyle, cardPathFn, fieldIDFn, getValFn) { - var key = f[0], label = f[1], type = f[2], hint = f[3], wide = f[4], showIf = f[5], searchEntity = f[6]; + var key = f[0], label = f[1], type = f[2], hint = f[3], wide = f[4], showIf = f[5], searchEntity = f[6], tooltip = f[7]; var fp = subPath ? subPath + '.' + key : key; var fid = fieldIDFn(sec, fp, idx); var val; @@ -343,8 +343,13 @@ function ced_renderField(sec, f, data, idx, subPath, optStyle, cardPathFn, field return ''; } + function lblSpan() { + if (tooltip) return '' + esc(label) + ''; + return '' + esc(label) + ''; + } + if (type === 'checkbox') { - return wrap('obj-field obj-check', ' ' + esc(label) + ''); + return wrap('obj-field obj-check', ' ' + lblSpan()); } if (type === 'json') { var jstr = typeof val === 'object' ? JSON.stringify(val) : String(val); @@ -356,7 +361,7 @@ function ced_renderField(sec, f, data, idx, subPath, optStyle, cardPathFn, field } if (type === 'select') { if (hint && hint.indexOf('dyn:') === 0) { - var inner = '' + esc(label) + '' + ced_buildDynamicSelect(fid, dpAttr, val, hint.substring(4)); + var inner = lblSpan() + ced_buildDynamicSelect(fid, dpAttr, val, hint.substring(4)); return wrap('obj-field', inner); } var opts = hint ? hint.split('|') : []; @@ -672,13 +677,11 @@ function ced_collectAtkType(widgetID) { function ced_renderTagHelp(type) { var h = '
'; if (type === 'color') { - h += 'Name is displayed without an article (e.g. "John Smith" instead of "a John Smith")
'; - h += 'Cannot be attacked by players. Combat level hidden
'; h += 'Text fields support color tags: {NN}text{/} where NN is a hex color index (00–FF).'; } else if (type === 'crafting') { h += 'Message fields support template variables:
%i1 = first ingredient name
%i2 = second ingredient name
%n = output item name.
Color tags are also supported: {NN}text{/} where NN is a hex color index (00–FF).'; } else if (type === 'gather') { - h += 'Message fields support template variables: {name} = object name.
Color tags are also supported: {NN}text{/} where NN is a hex color index (00–FF).'; + h += 'Message fields support template variables: {name} = object name.
Color tags are also supported: {NN}text{/} where NN is a hex color index (00–FF).'; } h += '
'; return h; diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index 04ad20b..2283c91 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -52,7 +52,7 @@ var SECTIONS = [ ['level', 'Level', 'number', '', 'narrow'], ['xp', 'XP', 'number', '', 'narrow'], ['ticks_per_cycle', 'Ticks/Cycle', 'number', '', 'narrow'], - ['output_qty', 'Output Qty', 'number', '', 'narrow'], + ['output_qty', 'Output Qty', 'number', '', 'narrow', null, null, 'Quantity produced per cycle, only relevant for stackable items.'], ['station', 'Station', 'select', 'dyn:stations'], ['tool', 'Tool', 'select', 'dyn:tools'], ['fail', 'Fail Item', 'search', 'burnt_fish', '', null, 'items'], @@ -65,10 +65,9 @@ var SECTIONS = [ ['end_message', 'End Message', 'text', 'You\'ve finished smithing.', 'wide'], ], fieldGroups: [ - ['type', 'subtype', 'level', 'xp', 'ticks_per_cycle', 'output_qty'], - {note: 'Output Qty is the quantity produced per cycle, only relevant for stackable items.'}, + ['type', 'subtype', 'level', 'xp', 'ticks_per_cycle', 'output_qty'], ['station', 'tool'], - {label: 'Success', fields: ['success_base', 'success_per_level', 'success_cap', 'fail']}, + {label: 'Success', fields: ['success_base', 'success_per_level', 'success_cap']}, ['start_message', 'success_message', 'fail_message', 'end_message'], ], subSubtables: [ @@ -120,6 +119,7 @@ var SECTIONS = [ ] }, ]; +var SKILL_KEYS = ['accuracy','strength','defense','hitpoints','ranged','science','technology','fishing','cooking','woodcutting','firemaking','mining','smithing','crafting','fletching','pharmacy','thieving','agility','construction','scavenging','hacking','assassin','farming']; function initItemEditor() { window._ieRenderCurrent = renderCurrent; @@ -220,7 +220,7 @@ function renderItemEditor(id, data) { setupCraftTypeChange(); setupEquipTypeChange(); updateOutputQtyVisibility(); - DropView.setupAllInter(); + if (typeof DropView !== 'undefined') DropView.setupAllInter(); setTimeout(function() { document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); }); }, 50); @@ -333,7 +333,16 @@ function renderCoreCard(data) { h += renderField(sec, sec.fields[3], data, -1); h += ''; h += '
'; + var qualFid = fieldID(sec, 'max_quality', -1); + var qualVal = data.max_quality; + var qgid = 'qg_core_quality'; + var qShow = qualVal ? '' : 'display:none;'; + var qBtnShow = qualVal ? 'display:none;' : ''; + h += ''; h += renderField(sec, sec.fields[4], data, -1); + h += ''; + h += ''; + h += ''; h += '
'; h += ''; var descFid = fieldID(sec, 'description', -1); @@ -509,7 +518,7 @@ function renderEquipmentCard(data) { // Requirements KV (at root level, stored in itemData.requirements) h += '
'; - h += renderKV({id: 'equipment_req'}, {label:'Requirements', key:'requirements', type:'kv', valType:'number'}, data); + h += renderKV({id: 'equipment_req'}, {label:'Requirements', key:'requirements', type:'kv', valType:'number'}, data, SKILL_KEYS); h += '
'; return h; @@ -763,6 +772,13 @@ function renderSubtable(sec, st, data) { h += '
'; renderGroup(group.fields); h += '
'; + var failF = st.fields.find(function(sf) { return sf[0] === 'fail'; }); + if (failF) { + var failVal = item.fail || ''; + var fgid = 'fg_' + sec.id + '_' + st.key + '_' + idx; + h += '
' + renderSubFieldHTML(sec, st.key, idx, failF, failVal) + '
'; + h += ''; + } h += ''; h += ''; } else { @@ -832,6 +848,7 @@ function addConsumePair(secID, subKey, rowIdx, subSubKey, subIdx) { var btnRow = document.querySelector('.ingredients-pair-add[data-sec="' + secID + '"][data-subkey="' + subKey + '"][data-row="' + rowIdx + '"][data-subsub="' + subSubKey + '"][data-subidx="' + subIdx + '"]'); var itemFid = ingredientPairID(sec, subKey, rowIdx, subSubKey, subIdx, pairIdx, 'items'); var bypFid = ingredientPairID(sec, subKey, rowIdx, subSubKey, subIdx, pairIdx, 'byproducts'); + var bpgid = 'bp_' + secID + '_' + subKey + '_' + rowIdx + '_' + subSubKey + '_' + subIdx + '_' + pairIdx; var row = document.createElement('div'); row.className = 'ingredients-pair'; row.setAttribute('data-sec', secID); @@ -843,7 +860,7 @@ function addConsumePair(secID, subKey, rowIdx, subSubKey, subIdx) { row.innerHTML = '
' + '
' + '
' + renderConsumeSearchHTML(itemFid, '', 'items', 'Search items...') + '
' - + '
' + renderConsumeSearchHTML(bypFid, '', 'items', '(optional)') + '
' + + '
' + '
'; btnRow.parentNode.insertBefore(row, btnRow); setupSearchFields(); @@ -914,7 +931,12 @@ function renderSubSubtable(sec, st, rowIdx, sst, parentItem) { var bypVal = bypArr[p] || ''; var bypFid = ingredientPairID(sec, st.key, rowIdx, sst.key, subIdx, p, 'byproducts'); - h += '
' + renderConsumeSearchHTML(bypFid, bypVal, 'items', '(optional)') + '
'; + var bpgid = 'bp_' + sec.id + '_' + st.key + '_' + rowIdx + '_' + sst.key + '_' + subIdx + '_' + p; + if (bypVal) { + h += '
' + renderConsumeSearchHTML(bypFid, bypVal, 'items', '(optional)') + '
'; + } else { + h += '
'; + } h += '
'; @@ -974,15 +996,28 @@ function renderTags(sec, il, data) { return ''; } -function renderKV(sec, il, data) { +function renderKV(sec, il, data, keyOptions) { var p = cardPath(sec); var sectionRoot = p ? (itemData[p] || {}) : itemData; var map = sectionRoot[il.key] || {}; + var hasKeyOpts = Array.isArray(keyOptions) && keyOptions.length > 0; var h = '
'; h += '' + esc(il.label) + ''; - h += '
'; + h += '
'; Object.keys(map).forEach(function(k, idx) { - h += '
'; + h += '
'; + if (hasKeyOpts) { + var keyOpts = keyOptions.slice(); + if (k && keyOptions.indexOf(k) === -1) keyOpts.unshift(k); + h += ''; + } else { + h += ''; + } h += ''; h += '
'; }); @@ -1192,7 +1227,19 @@ function addKVRow(cardID, subKey) { if (!el) return; var row = document.createElement('div'); row.className = 'obj-kv-row'; - row.innerHTML = ''; + var keyOptsStr = el.getAttribute('data-key-options'); + if (keyOptsStr) { + var keyOpts = keyOptsStr.split(','); + var selHTML = ''; + row.innerHTML = selHTML + ''; + } else { + row.innerHTML = ''; + } el.appendChild(row); } @@ -1292,9 +1339,6 @@ function updateOutputQtyVisibility() { 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() { ced_setupSearchFields(); } diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 5e2b9cc..05d33da 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -11,8 +11,8 @@ var MOB_SECTIONS = [ fields: [ ['name', 'Name', 'text', 'spaceport attendant'], ['description', 'Description', 'textarea', 'A lanky man in a wrinkled grey uniform.'], - ['unique', 'Unique', 'checkbox'], - ['protected', 'Protected', 'checkbox'], + ['unique', 'Unique', 'checkbox', '', '', null, null, 'Mob name is displayed without an article (e.g. "John Smith" instead of "a John Smith")'], + ['protected', 'Protected', 'checkbox', '', '', null, null, 'Cannot be attacked.'], ], inline: [ {label:'Idle Descriptions', key:'idle_descriptions', type:'tags'}, @@ -53,7 +53,6 @@ var MOB_SECTIONS = [ ['size', 'Size', 'text', 'small'], ], inline: [ - {label:'Combat Descriptions', key:'combat_descriptions', type:'tags'}, ] }, { @@ -437,9 +436,23 @@ function renderMobCombatCard(data) { h += renderMobField(sec, byKey['damage_without'], data, -1); h += '
'; - sec.inline.forEach(function(il) { - h += ced_renderTags(sec, il, data); + var combat = data.combat || {}; + var combatDescs = Array.isArray(combat.combat_descriptions) ? combat.combat_descriptions : []; + var cdgid = 'cdg_combat'; + var hasCD = combatDescs.length > 0; + h += ''; + h += ''; return h; } @@ -834,6 +847,16 @@ function addMobLootRow(cardID, subKey, kind) { renderCurrent(); } +function addCombatDescRow(cdgid) { + var list = document.querySelector('#' + cdgid + ' .obj-combat-desc-list'); + if (!list) return; + var row = document.createElement('div'); + row.className = 'obj-combat-desc-row'; + row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px'; + row.innerHTML = ''; + list.appendChild(row); +} + // ---- Save ---- function saveMob() { @@ -901,6 +924,17 @@ function saveMob() { 'stats.bonuses.science_percent_bonus','stats.bonuses.ranged_bonus','stats.bonuses.ranged_strength_bonus']; taskStrip.forEach(function(k) { delete sectionObj[k]; }); } + var descRows = document.querySelectorAll('.obj-combat-desc'); + if (descRows.length > 0) { + var descs = []; + descRows.forEach(function(inp) { + var v = inp.value.trim(); + if (v) descs.push(v); + }); + if (descs.length > 0) { + sectionObj['combat_descriptions'] = descs; + } + } } if (sec.subtables) { diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 6ae1dba..363cca9 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -70,14 +70,14 @@ var SECTIONS = [ id: 'safespot', label: 'Safespot', path: 'safespot', detect: function(d) { return d.safespot; }, fields: [ - ['tier', 'Tier', 'number', '1', 'narrow'], - ['max_block_size', 'Max Block Size', 'select', '|small|medium|large|massive'], + ['tier', 'Tier', 'number', '1', 'narrow', null, null, 'Player tier must be >= safespot tier to hide (+1 with unlocked_rock_cover flag)'], + ['max_block_size', 'Max Block Size', 'select', '|small|medium|large|massive', null, null, null, 'Blocks mobs smaller than this size. Empty = blocks everything, massive blocks all four sizes.'], ['max_occupants', 'Max Occupants', 'number', '1', 'narrow'], - ['unsafe_chance', 'Unsafe Chance', 'number', '0.05', 'narrow'], - ['decay_ticks', 'Decay Ticks', 'number', '100', 'narrow'], - ['decay_chance', 'Decay Chance', 'number', '0.01', 'narrow'], - ['respawn_ticks', 'Respawn Ticks', 'number', '50', 'narrow'], - ['respawn_on_hide', 'Respawn on hide', 'checkbox'], + ['unsafe_chance', 'Unsafe Chance', 'number', '0.05', 'narrow', null, null, 'Per-tick probability of being forced out of cover (0 = never)'], + ['decay_ticks', 'Decay Ticks', 'number', '100', 'narrow', null, null, 'Forced degrade after N ticks of use. 0 disables this decay path'], + ['decay_chance', 'Decay Chance', 'number', '0.01', 'narrow', null, null, 'Per-tick random degrade chance. 0 disables this decay path'], + ['respawn_ticks', 'Respawn Ticks', 'number', '50', 'narrow', null, null, 'Auto-reforms cover after N ticks when fully destroyed. 0 = permanently removed'], + ['respawn_on_hide', 'Respawn on hide', 'checkbox', '', '', null, null, 'Resets cover to full levels when first person hides on an empty/unused cover'], ], subtables: [ {label:'Levels of Decay (lower is more decayed)', key:'levels', vertical: true, fields:[ @@ -321,27 +321,6 @@ function renderCard(sec, data) { }); } - if (sec.id === 'safespot') { - var helpCollapsed = window._safespotHelpCollapsed === undefined ? true : window._safespotHelpCollapsed; - h += '
'; - h += '
'; - h += '' + (helpCollapsed ? '▶' : '▼') + ''; - h += 'How Tiers, Levels, Decay & Respawn Work'; - h += '
'; - if (!helpCollapsed) { - h += '
'; - h += 'Tier — Player tier (base 1; +1 with unlocked_rock_cover flag) must be ≥ safespot tier to hide.
'; - h += 'Levels of Decay — Degradation stages (N levels above all subtable entries). Level messages shown when hiding; degrade messages broadcast when damaged. Level 0 = destroyed, all occupants evicted.
'; - h += 'Decay — Two independent paths: decay_ticks (forced degrade after N ticks of use) and decay_chance (per-tick random degrade). Either or both can be set; 0 disables that path.
'; - h += 'Respawn on hide — Resets the cover to full levels when the first person hides on an empty/unused cover.
'; - h += 'Respawn ticks — Auto-reforms the cover after N ticks when fully destroyed (level 0). 0 = instance permanently removed.
'; - h += 'Max Block Size — Determines which mob sizes are blocked (small < medium < large < massive). Empty = blocks everything.
'; - h += 'Unsafe Chance — Per-tick probability of being forced out of cover regardless of decay (0 = never forced out).'; - h += '
'; - } - h += '
'; - } - if (sec.id === 'gather') { h += ced_renderTagHelp('gather'); } @@ -776,15 +755,6 @@ function hideBaitField() { renderObjectEditor(objectID, objectData); } -function toggleSafespotHelp() { - if (window._safespotHelpCollapsed === undefined) { - window._safespotHelpCollapsed = false; - } else { - window._safespotHelpCollapsed = !window._safespotHelpCollapsed; - } - renderCurrent(); -} - function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; diff --git a/internal/admin/templates/items.html b/internal/admin/templates/items.html index f1c8ee5..d81fb1d 100644 --- a/internal/admin/templates/items.html +++ b/internal/admin/templates/items.html @@ -12,6 +12,7 @@ +