diff options
| author | historia <[not public]> | 2026-07-03 16:55:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-03 16:55:23 -0400 |
| commit | 034187d1c434b6bee32aeb539ea543f6ec537376 (patch) | |
| tree | f238a9c6a657af20edf0c53e9d0980bc58c1133a /internal | |
| parent | 778157981cccb8cb4856e3ab5a72085830c59120 (diff) | |
| download | thehouseoficarus-034187d1c434b6bee32aeb539ea543f6ec537376.tar.gz | |
feat: card ui added to drops, hazards, techs, and mods in admin gui
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/static/admin.css | 2 | ||||
| -rw-r--r-- | internal/admin/static/dropeditor.js | 224 | ||||
| -rw-r--r-- | internal/admin/static/hazardeditor.js | 226 | ||||
| -rw-r--r-- | internal/admin/static/itemeditor.js | 11 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 13 | ||||
| -rw-r--r-- | internal/admin/static/moduleeditor.js | 318 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 11 | ||||
| -rw-r--r-- | internal/admin/static/techeditor.js | 191 | ||||
| -rw-r--r-- | internal/admin/templates/drops.html | 6 | ||||
| -rw-r--r-- | internal/admin/templates/hazards.html | 6 | ||||
| -rw-r--r-- | internal/admin/templates/modules.html | 6 | ||||
| -rw-r--r-- | internal/admin/templates/techs.html | 6 |
12 files changed, 1010 insertions, 10 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index b44adcd..8c3ee8e 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -272,6 +272,8 @@ g.ud-hover:hover text{font-weight:bold} .obj-card-arrow{font-size:10px;width:14px;color:#888} .obj-card-label{flex:1;color:var(--text)} .obj-card-remove{padding:2px 6px;font-size:10px;min-width:auto} +.obj-card-expand{padding:2px 5px;font-size:11px;min-width:auto;background:transparent;color:#888;border:1px solid transparent;border-radius:3px;cursor:pointer;font-family:monospace;line-height:1} +.obj-card-expand:hover{color:var(--text);border-color:var(--border);background:rgba(255,255,255,.05)} .obj-card-body{padding:10px 12px} .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} diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js new file mode 100644 index 0000000..1569a04 --- /dev/null +++ b/internal/admin/static/dropeditor.js @@ -0,0 +1,224 @@ +var dropData = null; +var dropID = null; +var expandedCards = {}; +var cardWidths = {}; + +var DROP_SECTIONS = [ + { + id: 'drops', label: 'Drops', always: true, path: 'drops', isArray: true, + fields: [ + ['item_id', 'Item ID', 'search', '', '', null, 'items'], + ['table', 'Table', 'search', '', '', null, 'drops'], + ['weight', 'Weight', 'number', '', 'narrow'], + ['quantity', 'Quantity', 'number', '1', 'narrow'], + ['level', 'Level', 'number', '', 'narrow'], + ['xp', 'XP', 'number', '', 'narrow'], + ['depletes', 'Depletes', 'checkbox'], + ['success_message', 'Success Message', 'text', '', 'wide'], + ] + } +]; + +function initDropEditor() { + editorType = 'drops'; + editorFields = []; + loadList(); + if (window.location.hash) loadDropEditor(window.location.hash.substring(1)); +} + +function cardPath(sec) { return sec.path || ''; } + +function getVal(data, sec, key) { + return ced_getVal(data, sec, key); +} + +function fieldID(sec, key, idx) { + return ced_fieldID(sec, key, idx); +} + +function setupSearchFields() { ced_setupSearchFields(); } + +function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); } + +function loadDropEditor(id) { + dropID = id; + currentID = id; + renderList(''); + window.location.hash = id; + API.get('/api/drops/' + encodeURIComponent(id)).then(function(data) { + dropData = data; + renderDropEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; + }); +} + +function loadItem(id) { loadDropEditor(id); } + +function renderDropEditor(id, data) { + var html = '<h2>Drop Table: ' + esc(id) + '</h2>'; + html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; + html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; + + html += '<div class="tab-content" id="tabFields">'; + html += '<div class="obj-fields-wrap">'; + DROP_SECTIONS.forEach(function(sec) { + html += renderCard(sec, data); + }); + html += '</div>'; + html += '</div>'; + + html += '<div class="tab-content" id="tabYaml" style="display:none">'; + html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>'; + html += '</div>'; + + html += '<div class="btn-row">'; + html += '<button class="btn btn-primary" onclick="saveDrop()">Save</button>'; + html += '<button class="btn btn-danger" onclick="deleteDrop()">Delete</button>'; + html += '</div>'; + + $('#editorMain').innerHTML = html; + setupSearchFields(); +} + +function renderCurrent() { + if (!dropData || !dropID) return; + renderDropEditor(dropID, dropData); +} + +function renderCard(sec, data) { + var collapsed = expandedCards[sec.id] === false; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; + h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; + h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; + h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '</div>'; + if (!collapsed) { + h += '<div class="obj-card-body">'; + h += renderArraySection(sec, data); + h += '</div>'; + } + h += '</div>'; + return h; +} + +function renderArraySection(sec, data) { + var arr = dropData[cardPath(sec)] || []; + var h = ''; + arr.forEach(function(item, idx) { + h += '<div class="obj-sub-row" data-idx="' + idx + '">'; + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, idx); + }); + h += '</div>'; + h += '<button class="btn btn-sm btn-danger obj-sub-remove" onclick="removeArrayItem(\'' + sec.id + '\',' + idx + ')">X</button>'; + h += '</div>'; + }); + h += '<button class="btn btn-sm obj-sub-add" onclick="addArrayItem(\'' + sec.id + '\')">+ Add Entry</button>'; + return h; +} + +function renderField(sec, f, data, idx) { + return ced_renderField(sec, f, data, idx, null, null, cardPath, fieldID, getVal); +} + +function toggleCard(id) { + expandedCards[id] = expandedCards[id] === false ? true : false; + renderCurrent(); +} + +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + +function addArrayItem(cardID) { + var sec = DROP_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = dropData[sec.path] || []; + var empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + arr.push(empty); + dropData[sec.path] = arr; + renderCurrent(); +} + +function removeArrayItem(cardID, idx) { + var sec = DROP_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = dropData[sec.path] || []; + arr.splice(idx, 1); + renderCurrent(); +} + +function saveDrop() { + if (!ced_validateFields(DROP_SECTIONS, fieldID, null)) return; + + var out = {}; + + DROP_SECTIONS.forEach(function(sec) { + if (sec.isArray) { + var arr = []; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row'); + rows.forEach(function(row, idx) { + var item = {}; + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0], idx); + var el = document.getElementById(fid); + if (el) item[f[0]] = collectFieldValue(el, f[2]); + }); + if (Object.keys(item).length > 0) arr.push(item); + }); + if (arr.length > 0) out[sec.path] = arr; + return; + } + + var sectionObj = {}; + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0]); + var el = document.getElementById(fid); + if (!el) return; + sectionObj[f[0]] = collectFieldValue(el, f[2]); + }); + + if (sec.always && sec.path) { + out[sec.path] = sectionObj; + } else if (sec.always) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } else if (sec.path) { + out[sec.path] = sectionObj; + } else { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } + }); + + ced_cleanOutput(out); + out.id = dropID; + + API.put('/api/drops/' + encodeURIComponent(dropID), out).then(function(resp) { + notify('Drop table ' + dropID + ' saved', 'success'); + updateUndoBar(); + if (resp && resp._raw) dropData._raw = resp._raw; + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function deleteDrop() { + if (!confirm('Delete drop table "' + dropID + '"?')) return; + API.del('/api/drops/' + encodeURIComponent(dropID)).then(function() { + notify('Drop table ' + dropID + ' deleted', 'success'); + updateUndoBar(); + dropID = null; + currentID = null; + dropData = 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'); }); +} diff --git a/internal/admin/static/hazardeditor.js b/internal/admin/static/hazardeditor.js new file mode 100644 index 0000000..b07456e --- /dev/null +++ b/internal/admin/static/hazardeditor.js @@ -0,0 +1,226 @@ +var hazardData = null; +var hazardID = null; +var expandedCards = {}; +var addedSections = {}; +var cardWidths = {}; + +var HAZARD_SECTIONS = [ + { + id: 'core', label: 'Core', always: true, + fields: [ + ['name', 'Name', 'text'], + ['description', 'Description', 'textarea', '', 'wide'], + ['warn_message', 'Warn Message', 'text', '', 'wide'], + ['hit_message', 'Hit Message', 'text'], + ['miss_message', 'Miss Message', 'text'], + ['required_item', 'Required Item', 'text'], + ['speed', 'Speed', 'number', '', 'narrow'], + ] + }, + { + id: 'combat', label: 'Combat', + detect: function(d) { return d.attack_type || d.attack || d.attack_bonus || d.max_hit; }, + fields: [ + ['attack_type', 'Attack Type', 'select', 'stab|slash|crush|ranged|science'], + ['attack', 'Attack', 'number', '', 'narrow'], + ['attack_bonus', 'Attack Bonus', 'number', '', 'narrow'], + ['max_hit', 'Max Hit', 'number', '', 'narrow'], + ] + } +]; + +function initHazardEditor() { + editorType = 'hazards'; + editorFields = []; + loadList(); + if (window.location.hash) loadHazardEditor(window.location.hash.substring(1)); +} + +function cardPath(sec) { return sec.path || ''; } + +function getVal(data, sec, key) { + return ced_getVal(data, sec, key); +} + +function fieldID(sec, key, idx) { + return ced_fieldID(sec, key, idx); +} + +function setupSearchFields() { ced_setupSearchFields(); } + +function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); } + +function loadHazardEditor(id) { + hazardID = id; + currentID = id; + renderList(''); + window.location.hash = id; + API.get('/api/hazards/' + encodeURIComponent(id)).then(function(data) { + hazardData = data; + renderHazardEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; + }); +} + +function loadItem(id) { loadHazardEditor(id); } + +function renderHazardEditor(id, data) { + var html = '<h2>Hazard: ' + esc(id) + '</h2>'; + html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; + html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; + + html += '<div class="tab-content" id="tabFields">'; + html += '<div class="obj-fields-wrap">'; + HAZARD_SECTIONS.forEach(function(sec) { + if (!sec.always && sec.detect && !sec.detect(data) && !addedSections[sec.id]) return; + html += renderCard(sec, data); + }); + html += '</div>'; + + html += '<div class="add-section-bar">'; + html += '<select id="addSectionSelect">'; + html += '<option value="">+ Add Section...</option>'; + HAZARD_SECTIONS.forEach(function(sec) { + if (sec.always) return; + if (sec.detect && sec.detect(data)) return; + if (addedSections[sec.id]) return; + html += '<option value="' + sec.id + '">' + esc(sec.label) + '</option>'; + }); + html += '</select>'; + html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; + html += '</div>'; + html += '</div>'; + + html += '<div class="tab-content" id="tabYaml" style="display:none">'; + html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>'; + html += '</div>'; + + html += '<div class="btn-row">'; + html += '<button class="btn btn-primary" onclick="saveHazard()">Save</button>'; + html += '<button class="btn btn-danger" onclick="deleteHazard()">Delete</button>'; + html += '</div>'; + + $('#editorMain').innerHTML = html; + setupSearchFields(); +} + +function renderCurrent() { + if (!hazardData || !hazardID) return; + renderHazardEditor(hazardID, hazardData); +} + +function renderCard(sec, data) { + var collapsed = expandedCards[sec.id] === false; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; + h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; + h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; + h += '<span class="obj-card-label">' + sec.label + '</span>'; + if (!sec.always) { + h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; + } + h += '</div>'; + if (!collapsed) { + h += '<div class="obj-card-body">'; + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, -1); + }); + h += '</div>'; + h += '</div>'; + } + h += '</div>'; + return h; +} + +function renderField(sec, f, data, idx) { + return ced_renderField(sec, f, data, idx, null, null, cardPath, fieldID, getVal); +} + +function toggleCard(id) { + expandedCards[id] = expandedCards[id] === false ? true : false; + renderCurrent(); +} + +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + +function removeSection(id) { + var sec = HAZARD_SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + if (sec.fields) sec.fields.forEach(function(f) { delete hazardData[f[0]]; }); + delete addedSections[id]; + renderCurrent(); +} + +function addSection() { + var sel = $('#addSectionSelect'); + var id = sel.value; + if (!id) return; + var sec = HAZARD_SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + + if (id === 'combat') { + hazardData.attack_type = ''; + hazardData.attack = 0; + hazardData.attack_bonus = 0; + hazardData.max_hit = 0; + } + addedSections[id] = true; + expandedCards[id] = true; + renderCurrent(); +} + +function saveHazard() { + if (!ced_validateFields(HAZARD_SECTIONS, fieldID, null)) return; + + var out = {}; + HAZARD_SECTIONS.forEach(function(sec) { + var sectionObj = {}; + var hasValues = sec.always; + + if (sec.fields) { + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0]); + var el = document.getElementById(fid); + if (!el) return; + var val = collectFieldValue(el, f[2]); + if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true; + sectionObj[f[0]] = val; + }); + } + + if (sec.always) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } else if (hasValues) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } + }); + + ced_cleanOutput(out); + out.id = hazardID; + + API.put('/api/hazards/' + encodeURIComponent(hazardID), out).then(function(resp) { + notify('Hazard ' + hazardID + ' saved', 'success'); + updateUndoBar(); + if (resp && resp._raw) hazardData._raw = resp._raw; + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function deleteHazard() { + if (!confirm('Delete hazard "' + hazardID + '"?')) return; + API.del('/api/hazards/' + encodeURIComponent(hazardID)).then(function() { + notify('Hazard ' + hazardID + ' deleted', 'success'); + updateUndoBar(); + hazardID = null; + currentID = null; + hazardData = 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'); }); +} diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index 215baf0..641f064 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -2,6 +2,7 @@ var itemData = null; var itemID = null; var expandedCards = {}; var addedSections = {}; +var cardWidths = {}; var SECTIONS = [ { @@ -231,9 +232,12 @@ function renderCurrent() { function renderCard(sec, data) { var collapsed = expandedCards[sec.id] === false; - var h = '<div class="obj-card" data-card="' + sec.id + '">'; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; @@ -868,6 +872,11 @@ function toggleCard(id) { renderCurrent(); } +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 4895c6f..4260e49 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -3,6 +3,7 @@ var mobData = null; var mobID = null; var expandedCards = {}; var addedSections = {}; +var cardWidths = {}; var MOB_SECTIONS = [ { @@ -76,7 +77,7 @@ var MOB_SECTIONS = [ ] }, { - id: 'talk', label: 'Talk', path: 'talk', + id: 'talk', label: 'Talk', path: 'talk', fullWidth: true, detect: function(d) { return d.talk && d.talk.nodes; }, fields: [] }, @@ -206,9 +207,12 @@ function renderCurrent() { function renderMobCard(sec, data) { var collapsed = expandedCards[sec.id] === false; - var h = '<div class="obj-card' + (sec.id === 'talk' ? ' obj-card-full' : '') + '" data-card="' + sec.id + '">'; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleMobCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleMobCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeMobSection(\'' + sec.id + '\')">X</button>'; @@ -555,6 +559,11 @@ function toggleMobCard(id) { renderCurrent(); } +function toggleMobCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + function removeMobSection(id) { var sec = MOB_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js new file mode 100644 index 0000000..128148d --- /dev/null +++ b/internal/admin/static/moduleeditor.js @@ -0,0 +1,318 @@ +var moduleData = null; +var moduleID = null; +var expandedCards = {}; +var addedSections = {}; +var cardWidths = {}; + +var MODULE_SECTIONS = [ + { + id: 'core', label: 'Core', always: true, + fields: [ + ['name', 'Name', 'text'], + ['level', 'Level', 'number', '', 'narrow'], + ['max_hit', 'Max Hit', 'number', '', 'narrow'], + ['base_xp', 'Base XP', 'number', '', 'narrow'], + ['category', 'Category', 'select', 'combat|utility|enchant|processing|transport'], + ['element', 'Element', 'text'], + ['destination', 'Destination', 'number', '', 'narrow'], + ], + inline: [ + {label:'Junk Cost', key:'junk_cost', type:'kv', valType:'number'}, + ] + }, + { + id: 'sequence', label: 'Sequence', path: 'sequence', isArray: true, + detect: function(d) { return d.sequence && Array.isArray(d.sequence); }, + fields: [ + ['message', 'Message', 'text', '', 'wide'], + ['delay', 'Delay', 'number', '', 'narrow'], + ] + } +]; + +function initModuleEditor() { + editorType = 'modules'; + editorFields = []; + loadList(); + if (window.location.hash) loadModuleEditor(window.location.hash.substring(1)); +} + +function cardPath(sec) { return sec.path || ''; } + +function getVal(data, sec, key) { + return ced_getVal(data, sec, key); +} + +function fieldID(sec, key, idx) { + return ced_fieldID(sec, key, idx); +} + +function setupSearchFields() { ced_setupSearchFields(); } + +function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); } + +function loadModuleEditor(id) { + moduleID = id; + currentID = id; + renderList(''); + window.location.hash = id; + API.get('/api/modules/' + encodeURIComponent(id)).then(function(data) { + moduleData = data; + renderModuleEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; + }); +} + +function loadItem(id) { loadModuleEditor(id); } + +function renderModuleEditor(id, data) { + var html = '<h2>Module: ' + esc(id) + '</h2>'; + html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; + html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; + + html += '<div class="tab-content" id="tabFields">'; + html += '<div class="obj-fields-wrap">'; + MODULE_SECTIONS.forEach(function(sec) { + if (!sec.always && sec.detect && !sec.detect(data) && !addedSections[sec.id]) return; + html += renderCard(sec, data); + }); + html += '</div>'; + + html += '<div class="add-section-bar">'; + html += '<select id="addSectionSelect">'; + html += '<option value="">+ Add Section...</option>'; + MODULE_SECTIONS.forEach(function(sec) { + if (sec.always) return; + if (sec.detect && sec.detect(data)) return; + if (addedSections[sec.id]) return; + html += '<option value="' + sec.id + '">' + esc(sec.label) + '</option>'; + }); + html += '</select>'; + html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; + html += '</div>'; + html += '</div>'; + + html += '<div class="tab-content" id="tabYaml" style="display:none">'; + html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>'; + html += '</div>'; + + html += '<div class="btn-row">'; + html += '<button class="btn btn-primary" onclick="saveModule()">Save</button>'; + html += '<button class="btn btn-danger" onclick="deleteModule()">Delete</button>'; + html += '</div>'; + + $('#editorMain').innerHTML = html; + setupSearchFields(); +} + +function renderCurrent() { + if (!moduleData || !moduleID) return; + renderModuleEditor(moduleID, moduleData); +} + +function renderCard(sec, data) { + var collapsed = expandedCards[sec.id] === false; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; + h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; + h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; + h += '<span class="obj-card-label">' + sec.label + '</span>'; + if (!sec.always) { + h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; + } + h += '</div>'; + if (!collapsed) { + h += '<div class="obj-card-body">'; + + if (sec.isArray) { + h += renderArraySection(sec, data); + } else { + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, -1); + }); + h += '</div>'; + + if (sec.inline) { + sec.inline.forEach(function(il) { + if (il.type === 'kv') { + h += ced_renderKV(sec, il, data); + } + }); + } + } + h += '</div>'; + } + h += '</div>'; + return h; +} + +function renderArraySection(sec, data) { + var arr = moduleData[cardPath(sec)] || []; + var h = ''; + arr.forEach(function(item, idx) { + h += '<div class="obj-sub-row" data-idx="' + idx + '">'; + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, idx); + }); + h += '</div>'; + h += '<button class="btn btn-sm btn-danger obj-sub-remove" onclick="removeArrayItem(\'' + sec.id + '\',' + idx + ')">X</button>'; + h += '</div>'; + }); + h += '<button class="btn btn-sm obj-sub-add" onclick="addArrayItem(\'' + sec.id + '\')">+ Add Step</button>'; + return h; +} + +function renderField(sec, f, data, idx) { + return ced_renderField(sec, f, data, idx, null, null, cardPath, fieldID, getVal); +} + +function toggleCard(id) { + expandedCards[id] = expandedCards[id] === false ? true : false; + renderCurrent(); +} + +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + +function removeSection(id) { + var sec = MODULE_SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + if (sec.path) { + delete moduleData[sec.path]; + } + delete addedSections[id]; + renderCurrent(); +} + +function addSection() { + var sel = $('#addSectionSelect'); + var id = sel.value; + if (!id) return; + var sec = MODULE_SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + + if (sec.path && sec.isArray) { + moduleData[sec.path] = []; + } else if (sec.path) { + moduleData[sec.path] = {}; + } + addedSections[id] = true; + expandedCards[id] = true; + renderCurrent(); +} + +function addArrayItem(cardID) { + var sec = MODULE_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = moduleData[sec.path] || []; + var empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + arr.push(empty); + moduleData[sec.path] = arr; + renderCurrent(); +} + +function removeArrayItem(cardID, idx) { + var sec = MODULE_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = moduleData[sec.path] || []; + arr.splice(idx, 1); + renderCurrent(); +} + +function saveModule() { + if (!ced_validateFields(MODULE_SECTIONS, fieldID, null)) return; + + var out = {}; + MODULE_SECTIONS.forEach(function(sec) { + if (sec.isArray) { + var arr = []; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row'); + rows.forEach(function(row, idx) { + var item = {}; + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0], idx); + var el = document.getElementById(fid); + if (el) item[f[0]] = collectFieldValue(el, f[2]); + }); + if (Object.keys(item).length > 0) arr.push(item); + }); + if (arr.length > 0) { out[sec.path] = arr; } + return; + } + + var sectionObj = {}; + var hasValues = sec.always; + + if (sec.fields) { + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0]); + var el = document.getElementById(fid); + if (!el) return; + var val = collectFieldValue(el, f[2]); + if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true; + sectionObj[f[0]] = val; + }); + } + + if (sec.inline) { + sec.inline.forEach(function(il) { + if (il.type === 'kv') { + var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]'); + if (kvEl) { + var map = {}; + kvEl.querySelectorAll('.obj-kv-row').forEach(function(row) { + var keyEl = row.querySelector('.obj-kv-key'); + var valEl = row.querySelector('.obj-kv-val'); + if (keyEl && keyEl.value.trim() && valEl) { + map[keyEl.value.trim()] = parseFloat(valEl.value) || 0; + } + }); + if (Object.keys(map).length > 0) { sectionObj[il.key] = map; hasValues = true; } + } + } + }); + } + + if (sec.always) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } else if (hasValues && sec.path) { + out[sec.path] = sectionObj; + } else if (hasValues) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } + }); + + ced_cleanOutput(out); + out.id = moduleID; + + API.put('/api/modules/' + encodeURIComponent(moduleID), out).then(function(resp) { + notify('Module ' + moduleID + ' saved', 'success'); + updateUndoBar(); + if (resp && resp._raw) moduleData._raw = resp._raw; + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function deleteModule() { + if (!confirm('Delete module "' + moduleID + '"?')) return; + API.del('/api/modules/' + encodeURIComponent(moduleID)).then(function() { + notify('Module ' + moduleID + ' deleted', 'success'); + updateUndoBar(); + moduleID = null; + currentID = null; + moduleData = 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'); }); +} diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index d45c26e..3cbb37a 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -1,6 +1,7 @@ var objectData = null; var objectID = null; var expandedCards = {}; +var cardWidths = {}; var SECTIONS = [ { @@ -220,9 +221,12 @@ function fieldIDSub(sec, subKey, idx, fieldKey) { function renderCard(sec, data) { var collapsed = expandedCards[sec.id] === false; - var h = '<div class="obj-card" data-card="' + sec.id + '">'; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; @@ -517,6 +521,11 @@ function toggleCard(id) { renderCurrent(); } +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js new file mode 100644 index 0000000..3d74254 --- /dev/null +++ b/internal/admin/static/techeditor.js @@ -0,0 +1,191 @@ +var techData = null; +var techID = null; +var expandedCards = {}; +var cardWidths = {}; + +var TECH_SECTIONS = [ + { + id: 'core', label: 'Core', always: true, + fields: [ + ['name', 'Name', 'text'], + ['level', 'Level', 'number', '', 'narrow'], + ['drain_rate', 'Drain Rate', 'number', '', 'narrow'], + ['category', 'Category', 'text'], + ['group', 'Group', 'text'], + ] + }, + { + id: 'effects', label: 'Effects', always: true, path: 'effects', + fields: [ + ['accuracy_percent', 'Accuracy %', 'number', '', 'narrow'], + ['strength_percent', 'Strength %', 'number', '', 'narrow'], + ['defense_percent', 'Defense %', 'number', '', 'narrow'], + ['ranged_percent', 'Ranged %', 'number', '', 'narrow'], + ['science_percent', 'Science %', 'number', '', 'narrow'], + ['damage_reduction', 'Damage Reduction', 'number', '', 'narrow'], + ['hp_regen_multi', 'HP Regen Multi', 'number', '', 'narrow'], + ['preserve_drain', 'Preserve Drain', 'number', '', 'narrow'], + ['retribution_pct', 'Retribution %', 'number', '', 'narrow'], + ['protect_melee', 'Protect Melee', 'checkbox'], + ['protect_ranged', 'Protect Ranged', 'checkbox'], + ['protect_science', 'Protect Science', 'checkbox'], + ] + } +]; + +function initTechEditor() { + editorType = 'techs'; + editorFields = []; + loadList(); + if (window.location.hash) loadTechEditor(window.location.hash.substring(1)); +} + +function cardPath(sec) { return sec.path || ''; } + +function getVal(data, sec, key) { + return ced_getVal(data, sec, key); +} + +function fieldID(sec, key, idx) { + return ced_fieldID(sec, key, idx); +} + +function setupSearchFields() { ced_setupSearchFields(); } + +function collectFieldValue(el, type) { return ced_collectFieldValue(el, type); } + +function loadTechEditor(id) { + techID = id; + currentID = id; + renderList(''); + window.location.hash = id; + API.get('/api/techs/' + encodeURIComponent(id)).then(function(data) { + techData = data; + renderTechEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>'; + }); +} + +function loadItem(id) { loadTechEditor(id); } + +function renderTechEditor(id, data) { + var html = '<h2>Tech: ' + esc(id) + '</h2>'; + html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; + html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; + + html += '<div class="tab-content" id="tabFields">'; + html += '<div class="obj-fields-wrap">'; + TECH_SECTIONS.forEach(function(sec) { + html += renderCard(sec, data); + }); + html += '</div>'; + html += '</div>'; + + html += '<div class="tab-content" id="tabYaml" style="display:none">'; + html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>'; + html += '</div>'; + + html += '<div class="btn-row">'; + html += '<button class="btn btn-primary" onclick="saveTech()">Save</button>'; + html += '<button class="btn btn-danger" onclick="deleteTech()">Delete</button>'; + html += '</div>'; + + $('#editorMain').innerHTML = html; + setupSearchFields(); +} + +function renderCurrent() { + if (!techData || !techID) return; + renderTechEditor(techID, techData); +} + +function renderCard(sec, data) { + var collapsed = expandedCards[sec.id] === false; + var isFull = cardWidths[sec.id] !== undefined ? cardWidths[sec.id] : (sec.fullWidth || false); + var fullClass = isFull ? ' obj-card-full' : ''; + var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; + h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; + h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; + h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '</div>'; + if (!collapsed) { + h += '<div class="obj-card-body">'; + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, -1); + }); + h += '</div>'; + h += '</div>'; + } + h += '</div>'; + return h; +} + +function renderField(sec, f, data, idx) { + return ced_renderField(sec, f, data, idx, null, null, cardPath, fieldID, getVal); +} + +function toggleCard(id) { + expandedCards[id] = expandedCards[id] === false ? true : false; + renderCurrent(); +} + +function toggleCardWidth(id) { + cardWidths[id] = !cardWidths[id]; + renderCurrent(); +} + +function saveTech() { + if (!ced_validateFields(TECH_SECTIONS, fieldID, null)) return; + + var out = {}; + TECH_SECTIONS.forEach(function(sec) { + var sectionObj = {}; + var hasValues = sec.always; + + if (sec.fields) { + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0]); + var el = document.getElementById(fid); + if (!el) return; + var val = collectFieldValue(el, f[2]); + if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true; + sectionObj[f[0]] = val; + }); + } + + if (sec.always && sec.path) { + out[sec.path] = sectionObj; + } else if (sec.always) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } else if (hasValues && sec.path) { + out[sec.path] = sectionObj; + } else if (hasValues) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + } + }); + + ced_cleanOutput(out); + out.id = techID; + + API.put('/api/techs/' + encodeURIComponent(techID), out).then(function(resp) { + notify('Tech ' + techID + ' saved', 'success'); + updateUndoBar(); + if (resp && resp._raw) techData._raw = resp._raw; + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function deleteTech() { + if (!confirm('Delete tech "' + techID + '"?')) return; + API.del('/api/techs/' + encodeURIComponent(techID)).then(function() { + notify('Tech ' + techID + ' deleted', 'success'); + updateUndoBar(); + techID = null; + currentID = null; + techData = 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'); }); +} diff --git a/internal/admin/templates/drops.html b/internal/admin/templates/drops.html index c040ef2..fb67c86 100644 --- a/internal/admin/templates/drops.html +++ b/internal/admin/templates/drops.html @@ -10,9 +10,9 @@ </div> </div> <script src="/static/editor.js"></script> +<script src="/static/cardeditor.js"></script> +<script src="/static/dropeditor.js"></script> <script> -initEditor('drops', [ - {key:'drops',label:'Drops (JSON array)',type:'textarea',parser:function(v){try{return JSON.parse(v)}catch(e){return []}}} -]); +initDropEditor(); </script> {{end}} diff --git a/internal/admin/templates/hazards.html b/internal/admin/templates/hazards.html index 0d60df6..d7d4548 100644 --- a/internal/admin/templates/hazards.html +++ b/internal/admin/templates/hazards.html @@ -10,5 +10,9 @@ </div> </div> <script src="/static/editor.js"></script> -<script>initEditor('hazards', [{key:'name',label:'Name',type:'text'},{key:'damage',label:'Damage',type:'number'},{key:'speed',label:'Speed',type:'number'},{key:'required_item',label:'Required Item',type:'text'},{key:'message',label:'Message',type:'text'},{key:'lethal',label:'Lethal',type:'checkbox'}]);</script> +<script src="/static/cardeditor.js"></script> +<script src="/static/hazardeditor.js"></script> +<script> +initHazardEditor(); +</script> {{end}} diff --git a/internal/admin/templates/modules.html b/internal/admin/templates/modules.html index 0f8ca7c..e46af1b 100644 --- a/internal/admin/templates/modules.html +++ b/internal/admin/templates/modules.html @@ -10,5 +10,9 @@ </div> </div> <script src="/static/editor.js"></script> -<script>initEditor('modules', [{key:'name',label:'Name',type:'text'},{key:'type',label:'Type',type:'text'},{key:'effect',label:'Effect',type:'text'},{key:'description',label:'Description',type:'textarea'}]);</script> +<script src="/static/cardeditor.js"></script> +<script src="/static/moduleeditor.js"></script> +<script> +initModuleEditor(); +</script> {{end}} diff --git a/internal/admin/templates/techs.html b/internal/admin/templates/techs.html index 084d564..813d57b 100644 --- a/internal/admin/templates/techs.html +++ b/internal/admin/templates/techs.html @@ -10,5 +10,9 @@ </div> </div> <script src="/static/editor.js"></script> -<script>initEditor('techs', [{key:'name',label:'Name',type:'text'},{key:'category',label:'Category',type:'text'},{key:'tier',label:'Tier',type:'number'},{key:'drain',label:'Drain Rate',type:'number'},{key:'bonus',label:'Bonus',type:'number'},{key:'description',label:'Description',type:'textarea'}]);</script> +<script src="/static/cardeditor.js"></script> +<script src="/static/techeditor.js"></script> +<script> +initTechEditor(); +</script> {{end}} |
