diff options
| author | historia <[not public]> | 2026-07-14 03:05:42 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-14 03:05:42 -0400 |
| commit | 430c81bd2056769e1326df9d1dff983afe7f33df (patch) | |
| tree | 83b7a7695c60519023b7634a3f552f66b34f7dbf /internal | |
| parent | d72957499f1e25741b8f7a91746fbe132fa68161 (diff) | |
| download | thehouseoficarus-430c81bd2056769e1326df9d1dff983afe7f33df.tar.gz | |
fix(admin): tech tab loading yaml and css
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/api_techs.go | 30 | ||||
| -rw-r--r-- | internal/admin/static/admin.css | 1 | ||||
| -rw-r--r-- | internal/admin/static/techeditor.js | 220 | ||||
| -rw-r--r-- | internal/game/core_validate.go | 1 | ||||
| -rw-r--r-- | internal/game/sys_technology.go | 1 | ||||
| -rw-r--r-- | internal/validate/checks.go | 7 | ||||
| -rw-r--r-- | internal/validate/validate.go | 6 |
7 files changed, 231 insertions, 35 deletions
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 += '<button class="btn btn-sm obj-card-expand" onclick="event.preventDefault();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; h += '</summary>'; 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>'; + + if (sec.id === 'core') { + h += renderTechCoreCard(data); + } else if (sec.id === 'effects') { + h += renderTechEffectsCard(data); + } else { + h += '<div class="obj-card-grid">'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, -1); + }); + h += '</div>'; + } + h += '</div>'; h += '</details>'; 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 += '<div class="obj-core-row">'; + 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 += '</div>'; + + h += '<div class="obj-core-row">'; + + var groupExists = data && data.group !== undefined; + if (groupExists) { + h += '<div style="display:flex;align-items:center;gap:4px">'; + h += renderField(sec, fields[3], data, -1); + h += '<button class="btn btn-sm btn-danger" style="padding:2px 6px;font-size:10px;min-width:auto;margin-top:14px" onclick="removeTechGroup()">X</button>'; + h += '</div>'; + } else { + h += '<div style="display:flex;align-items:center;margin-top:14px">'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addTechGroup()">+ Add Group</button>'; + h += '</div>'; + } + + h += '</div>'; + + h += '<div style="font-size:10px;color:#888;margin-bottom:4px">Only one tech per Group can be active at a time.</div>'; + h += '<div style="font-size:10px;color:#888;margin-bottom:8px">Drain rate is applied per tick.</div>'; + + 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 += '<div class="obj-inline-group">'; + h += '<div style="display:flex;align-items:flex-start;gap:6px">'; + h += '<div style="flex:1">'; + h += '<div class="obj-field">'; + h += '<span>Protection</span>'; + h += '<div class="seg-pill seg-pill-3 seg-idx-' + protectIdx + '" style="margin-top:4px" id="tech-protect-pill">'; + ['melee', 'ranged', 'science'].forEach(function(t, i) { + h += '<button type="button" class="seg-opt' + (i === protectIdx ? ' seg-active' : '') + '" data-v="' + t + '" onclick="techProtectPillSelect(\'' + t + '\',this)">' + t + '</button>'; + }); + h += '</div>'; + h += '</div>'; + h += '</div>'; + h += '<button class="btn btn-sm btn-danger" style="padding:2px 6px;font-size:10px;min-width:auto;margin-top:2px" onclick="removeTechProtection()">X</button>'; + h += '</div>'; + h += '</div>'; + } + + 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 += '<div class="obj-inline-group">'; + h += '<div style="display:flex;align-items:flex-start;gap:6px">'; + h += '<div style="flex:1">'; + h += renderField(sec, f, data, -1); + h += '</div>'; + h += '<button class="btn btn-sm btn-danger" style="padding:2px 6px;font-size:10px;min-width:auto;margin-top:2px" onclick="removeTechEffect(\'' + key + '\')">X</button>'; + h += '</div>'; + h += '</div>'; + }); + + 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 += '<div style="margin-top:8px"></div>'; + h += '<div class="obj-card-grid">'; + inactiveFields.forEach(function(f) { + var key = f[0], label = f[1], type = f[2]; + var dflt = type === 'checkbox' ? true : (parseFloat(f[3]) || 0); + h += '<button class="btn btn-sm obj-sub-add" style="margin-bottom:4px" onclick="addTechEffect(\'' + key + '\',' + dflt + ')">+ ' + esc(label) + '</button>'; + }); + + if (!protectType) { + h += '<button class="btn btn-sm obj-sub-add" style="margin-bottom:4px" onclick="addTechProtection()">+ Protection</button>'; + } + + h += '</div>'; + } + + 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'); diff --git a/internal/game/core_validate.go b/internal/game/core_validate.go index 4a2e5b9..0e302c0 100644 --- a/internal/game/core_validate.go +++ b/internal/game/core_validate.go @@ -32,7 +32,6 @@ func (g *Game) validationSource() validate.Source { techViews = append(techViews, validate.TechView{ ID: t.ID, Name: t.Name, - Category: t.Category, DrainRate: t.DrainRate, }) techIDs[t.ID] = true diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go index c7214af..e285ba8 100644 --- a/internal/game/sys_technology.go +++ b/internal/game/sys_technology.go @@ -33,7 +33,6 @@ type TechDef struct { Name string `yaml:"name"` Level int `yaml:"level"` DrainRate float64 `yaml:"drain_rate"` - Category string `yaml:"category"` Group string `yaml:"group"` Effects TechEffects `yaml:"effects"` } diff --git a/internal/validate/checks.go b/internal/validate/checks.go index 7ac6fa1..527b558 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -1068,13 +1068,6 @@ func validateTechs(s Source) []Issue { Message: fmt.Sprintf("Tech %q: has no name", def.ID), }) } - if def.Category != "" && !knownTechCategories[def.Category] { - issues = append(issues, Issue{ - Level: "WARN", - Type: "reference", - Message: fmt.Sprintf("Tech %q: unknown category %q", def.ID, def.Category), - }) - } if def.DrainRate < 0 { issues = append(issues, Issue{ Level: "ERROR", diff --git a/internal/validate/validate.go b/internal/validate/validate.go index 234651e..735c9ef 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -36,7 +36,6 @@ type CourseView struct { type TechView struct { ID string Name string - Category string DrainRate float64 } @@ -151,11 +150,6 @@ func LogIssues(issues []Issue) { } } -var knownTechCategories = map[string]bool{ - "accuracy": true, "strength": true, "defense": true, "ranged": true, - "science": true, "protection": true, "utility": true, "combo": true, -} - // reservedTechIDs are tech IDs whose semantics are coupled to code logic and // must exist in YAML (damage protection mapping + death retribution). var reservedTechIDs = map[string]bool{ |
