From 430c81bd2056769e1326df9d1dff983afe7f33df Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Tue, 14 Jul 2026 03:05:42 -0400
Subject: fix(admin): tech tab loading yaml and css
---
internal/admin/api_techs.go | 30 +++--
internal/admin/static/admin.css | 1 +
internal/admin/static/techeditor.js | 220 ++++++++++++++++++++++++++++++++++--
3 files changed, 231 insertions(+), 20 deletions(-)
(limited to 'internal/admin')
diff --git a/internal/admin/api_techs.go b/internal/admin/api_techs.go
index 0d3e201..293b1db 100644
--- a/internal/admin/api_techs.go
+++ b/internal/admin/api_techs.go
@@ -7,6 +7,14 @@ import (
"strings"
)
+func (s *AdminServer) findTechFile(id string) ([]byte, string, error) {
+ data, path, err := readYAMLFile(s.dataDir, "techs", id)
+ if err == nil {
+ return data, path, nil
+ }
+ return findYAMLFileInSubdirs(s.dataDir, "techs", id)
+}
+
func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
@@ -31,11 +39,11 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
return
}
delete(m, "id")
- path := filepath.Join(s.dataDir, "techs", id+".yaml")
- if _, err := os.Stat(path); err == nil {
+ if _, _, err := s.findTechFile(id); err == nil {
writeJSON(w, map[string]any{"error": "tech already exists"})
return
}
+ path := filepath.Join(s.dataDir, "techs", id+".yaml")
newContent, err := writeMapAsYAML(path, m)
if err != nil {
writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
@@ -62,7 +70,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- data, path, err := findYAMLFileInSubdirs(s.dataDir, "techs", id)
+ data, path, err := s.findTechFile(id)
if err != nil {
writeJSON(w, map[string]any{"error": "not found: " + id})
return
@@ -87,10 +95,17 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
delete(body, "_path")
delete(body, "_raw")
- path := filepath.Join(s.dataDir, "techs", id+".yaml")
+ _, path, findErr := s.findTechFile(id)
+ isCreate := findErr != nil
+ if isCreate {
+ path = filepath.Join(s.dataDir, "techs", id+".yaml")
+ }
+
var oldContent []byte
- if existing, err := snapshotFile(path); err == nil {
- oldContent = existing
+ if !isCreate {
+ if existing, err := snapshotFile(path); err == nil {
+ oldContent = existing
+ }
}
newContent, err := writeMapAsYAML(path, body)
@@ -99,7 +114,6 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
return
}
- isCreate := oldContent == nil
desc := "Updated tech " + id
if isCreate {
desc = "Created tech " + id
@@ -114,7 +128,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"ok": true, "id": id})
case http.MethodDelete:
- _, path, err := findYAMLFileInSubdirs(s.dataDir, "techs", id)
+ _, path, err := s.findTechFile(id)
if err != nil {
writeJSON(w, map[string]any{"error": "not found: " + id})
return
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index 3d54e54..8106773 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -146,6 +146,7 @@ input.search-input{padding-right:24px;background-image:url("data:image/svg+xml,%
.talk-orphans{margin-top:12px;padding-top:8px;border-top:1px dashed var(--border)}
.talk-orphans-label{font-size:10px;color:#f39c12;text-transform:uppercase;letter-spacing:.3px;margin-bottom:2px}
.talk-orphans-hint{font-size:9px;color:#888;margin-bottom:6px;font-style:italic}
+.tech-info-hint{font-size:10px;color:#888;margin-bottom:8px;font-style:italic}
.tabs{display:flex;gap:2px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}
.tab{padding:6px 14px;font-size:12px;background:transparent;color:#aaa;border:none;border-bottom:2px solid transparent;cursor:pointer;font-family:monospace}
.tab:hover{color:var(--text)}
diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js
index 020ba11..27dcbd8 100644
--- a/internal/admin/static/techeditor.js
+++ b/internal/admin/static/techeditor.js
@@ -10,8 +10,7 @@ var TECH_SECTIONS = [
['name', 'Name', 'text', 'Protect from Melee'],
['level', 'Level', 'number', '37', 'narrow'],
['drain_rate', 'Drain Rate', 'number', '0.5', 'narrow'],
- ['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'],
+ ['group', 'Group', 'text', 'accuracy_tier'],
]
},
{
@@ -26,9 +25,6 @@ var TECH_SECTIONS = [
['hp_regen_multi', 'HP Regen Multi', 'number', '2', 'narrow'],
['preserve_drain', 'Preserve Drain', 'number', '1', 'narrow'],
['retribution_pct', 'Retribution %', 'number', '25', 'narrow'],
- ['protect_melee', 'Protect Melee', 'checkbox'],
- ['protect_ranged', 'Protect Ranged', 'checkbox'],
- ['protect_science', 'Protect Science', 'checkbox'],
]
}
];
@@ -118,18 +114,26 @@ function renderCard(sec, data) {
h += '';
h += '';
h += '
';
- h += '
';
- sec.fields.forEach(function(f) {
- h += renderField(sec, f, data, -1);
- });
- h += '
';
+
+ if (sec.id === 'core') {
+ h += renderTechCoreCard(data);
+ } else if (sec.id === 'effects') {
+ h += renderTechEffectsCard(data);
+ } else {
+ h += '
';
+ sec.fields.forEach(function(f) {
+ h += renderField(sec, f, data, -1);
+ });
+ h += '
';
+ }
+
h += '
';
h += '';
return h;
}
-function renderField(sec, f, data, idx) {
- return ced_renderField(sec, f, data, idx, null, null, cardPath, fieldID, getVal);
+function renderField(sec, f, data, idx, subPath, optStyle) {
+ return ced_renderField(sec, f, data, idx, subPath, optStyle, cardPath, fieldID, getVal);
}
function toggleCardWidth(id) {
@@ -137,9 +141,191 @@ function toggleCardWidth(id) {
renderCurrent();
}
+// ---- Custom Core card ----
+
+function renderTechCoreCard(data) {
+ var sec = TECH_SECTIONS.find(function(s) { return s.id === 'core'; });
+ var fields = sec.fields;
+ var h = '';
+
+ h += '';
+ h += renderField(sec, fields[0], data, -1, null, 'flex:1');
+ h += renderField(sec, fields[1], data, -1);
+ h += renderField(sec, fields[2], data, -1);
+ h += '
';
+
+ h += '';
+
+ var groupExists = data && data.group !== undefined;
+ if (groupExists) {
+ h += '
';
+ h += renderField(sec, fields[3], data, -1);
+ h += '';
+ h += '
';
+ } else {
+ h += '
';
+ h += '';
+ h += '
';
+ }
+
+ h += '
';
+
+ h += 'Only one tech per Group can be active at a time.
';
+ h += 'Drain rate is applied per tick.
';
+
+ return h;
+}
+
+function addTechGroup() {
+ ced_syncDOMToData(techData);
+ techData.group = '';
+ renderTechEditor(techID, techData);
+}
+
+function removeTechGroup() {
+ ced_syncDOMToData(techData);
+ delete techData.group;
+ renderTechEditor(techID, techData);
+}
+
+// ---- Custom Effects card ----
+
+function renderTechEffectsCard(data) {
+ var sec = TECH_SECTIONS.find(function(s) { return s.id === 'effects'; });
+ var eff = data.effects || {};
+ var h = '';
+ var hasActive = false;
+
+ var protectType = null;
+ if (eff.protect_melee) protectType = 'melee';
+ else if (eff.protect_ranged) protectType = 'ranged';
+ else if (eff.protect_science) protectType = 'science';
+
+ if (protectType) {
+ hasActive = true;
+ var protectIdx = protectType === 'melee' ? 0 : (protectType === 'ranged' ? 1 : 2);
+ h += '';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
Protection';
+ h += '
';
+ ['melee', 'ranged', 'science'].forEach(function(t, i) {
+ h += '';
+ });
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ }
+
+ sec.fields.forEach(function(f) {
+ var key = f[0], label = f[1];
+ var val = eff[key];
+ if (val === undefined || val === null) return;
+ if (f[2] === 'checkbox' && !val) return;
+ if (val === '' || val === 0) return;
+
+ hasActive = true;
+ h += '';
+ h += '
';
+ h += '
';
+ h += renderField(sec, f, data, -1);
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ });
+
+ var inactiveFields = [];
+ sec.fields.forEach(function(f) {
+ var key = f[0];
+ var val = eff[key];
+ var isSet = val !== undefined && val !== null;
+ if (f[2] === 'checkbox' && !val) isSet = false;
+ if (val === '' || val === 0) isSet = false;
+ if (!isSet) inactiveFields.push(f);
+ });
+
+ if (inactiveFields.length > 0 || !protectType) {
+ if (hasActive) h += '';
+ h += '';
+ inactiveFields.forEach(function(f) {
+ var key = f[0], label = f[1], type = f[2];
+ var dflt = type === 'checkbox' ? true : (parseFloat(f[3]) || 0);
+ h += '';
+ });
+
+ if (!protectType) {
+ h += '';
+ }
+
+ h += '
';
+ }
+
+ return h;
+}
+
+function addTechEffect(key, dflt) {
+ ced_syncDOMToData(techData);
+ if (!techData.effects) techData.effects = {};
+ techData.effects[key] = dflt;
+ renderTechEditor(techID, techData);
+}
+
+function removeTechEffect(key) {
+ ced_syncDOMToData(techData);
+ if (techData.effects) delete techData.effects[key];
+ renderTechEditor(techID, techData);
+}
+
+function addTechProtection() {
+ ced_syncDOMToData(techData);
+ if (!techData.effects) techData.effects = {};
+ techData.effects.protect_melee = true;
+ techData.effects.damage_reduction = 0.4;
+ renderTechEditor(techID, techData);
+}
+
+function removeTechProtection() {
+ ced_syncDOMToData(techData);
+ if (techData.effects) {
+ delete techData.effects.protect_melee;
+ delete techData.effects.protect_ranged;
+ delete techData.effects.protect_science;
+ }
+ renderTechEditor(techID, techData);
+}
+
+function techProtectPillSelect(val, btn) {
+ ced_syncDOMToData(techData);
+ if (!techData.effects) techData.effects = {};
+ delete techData.effects.protect_melee;
+ delete techData.effects.protect_ranged;
+ delete techData.effects.protect_science;
+ techData.effects['protect_' + val] = true;
+
+ var pill = document.getElementById('tech-protect-pill');
+ if (pill) {
+ 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);
+ }
+}
+
+// ---- Save / Delete / Duplicate ----
+
function saveTech() {
if (!ced_validateFields(TECH_SECTIONS, fieldID, null)) return;
+ ced_syncDOMToData(techData);
+
var out = {};
TECH_SECTIONS.forEach(function(sec) {
var sectionObj = {};
@@ -167,8 +353,18 @@ function saveTech() {
}
});
+ if (techData.effects) {
+ if (techData.effects.protect_melee || techData.effects.protect_ranged || techData.effects.protect_science) {
+ if (!out.effects) out.effects = {};
+ if (techData.effects.protect_melee) out.effects.protect_melee = true;
+ if (techData.effects.protect_ranged) out.effects.protect_ranged = true;
+ if (techData.effects.protect_science) out.effects.protect_science = true;
+ }
+ }
+
ced_cleanOutput(out);
out.id = techID;
+ if (out.effects && Object.keys(out.effects).length === 0) delete out.effects;
API.put('/api/techs/' + encodeURIComponent(techID), out).then(function(resp) {
notify('Tech ' + techID + ' saved', 'success');
--
cgit v1.2.3