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