diff options
| author | historia <[not public]> | 2026-07-13 20:36:26 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-13 20:36:26 -0400 |
| commit | 5ea48c1f91298b42924f6865ffc30810cceec2d3 (patch) | |
| tree | f13ba8276e66493750b72124d75da8911311ff6f | |
| parent | 223d198c95b64d3d803be5d914c5b899dd30c8eb (diff) | |
| download | thehouseoficarus-5ea48c1f91298b42924f6865ffc30810cceec2d3.tar.gz | |
feat(admin): triggers organized by directory, support duplicate and delete
| -rw-r--r-- | data/.admin_history.json | 2 | ||||
| -rw-r--r-- | data/triggers/player_flag/announce_99_copy.yaml | 3 | ||||
| -rw-r--r-- | internal/admin/api_triggers.go | 60 | ||||
| -rw-r--r-- | internal/admin/static/triggerseditor.js | 165 |
4 files changed, 158 insertions, 72 deletions
diff --git a/data/.admin_history.json b/data/.admin_history.json index 85b92cf..6628a6e 100644 --- a/data/.admin_history.json +++ b/data/.admin_history.json @@ -1 +1 @@ -{"history":[{"time":"2026-07-13T19:53:39-04:00","description":"update room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"block_transport: false\ncolor: D3\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nhazard: \"\"\nid: 1010\nitem_spawns: []\nmobs: []\nname: 'Room #1010'\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: D3\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nhazard: \"\"\nid: 1010\nitem_spawns: []\nmobs: []\nname: 'Room #1010'\nobjects:\n - id: 000_test_object\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-13T19:53:49-04:00","description":"update room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"block_transport: false\ncolor: D3\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nhazard: \"\"\nid: 1010\nitem_spawns: []\nmobs: []\nname: 'Room #1010'\nobjects:\n - id: 000_test_object\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: D3\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nhazard: \"\"\nid: 1010\nitem_spawns: []\nmobs: []\nname: 'Room #1010'\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false}],"redo":null}
\ No newline at end of file +{"history":[{"time":"2026-07-13T20:31:17-04:00","description":"Created trigger announce_99_copy","file_path":"data/triggers/player_flag/announce_99_copy.yaml","old_content":"","new_content":"on_player_flag: \"\"\nsteps: []\n","is_delete":false,"is_create":true},{"time":"2026-07-13T20:31:17-04:00","description":"Update trigger announce_99_copy","file_path":"data/triggers/player_flag/announce_99_copy.yaml","old_content":"on_player_flag: \"\"\nsteps: []\n","new_content":"steps:\n - broadcast_global: '%p has reached level 99 %v!'\non_player_flag: announce_99_skill\n","is_delete":false,"is_create":false}],"redo":null}
\ No newline at end of file diff --git a/data/triggers/player_flag/announce_99_copy.yaml b/data/triggers/player_flag/announce_99_copy.yaml new file mode 100644 index 0000000..2c3f789 --- /dev/null +++ b/data/triggers/player_flag/announce_99_copy.yaml @@ -0,0 +1,3 @@ +steps: + - broadcast_global: '%p has reached level 99 %v!' +on_player_flag: announce_99_skill diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go index ccfd52d..749f5a1 100644 --- a/internal/admin/api_triggers.go +++ b/internal/admin/api_triggers.go @@ -13,23 +13,22 @@ import ( "thehouseoficarus/internal/behavior" ) -type triggerEntry struct { - ID string `json:"id"` - Kind string `json:"kind"` -} - func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - entries, err := listTriggerEntries(s.dataDir) + tree, err := listYAMLTree(s.dataDir, "triggers") if err != nil { + if os.IsNotExist(err) { + writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}}) + return + } writeJSON(w, map[string]any{"error": err.Error()}) return } - if entries == nil { - entries = []triggerEntry{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, entries) + writeJSON(w, tree) case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { @@ -200,49 +199,6 @@ func kindToSubdir(kind string) string { return "player_flag" } -func listTriggerEntries(dataDir string) ([]triggerEntry, error) { - root := filepath.Join(dataDir, "triggers") - if _, err := os.Stat(root); os.IsNotExist(err) { - return []triggerEntry{}, nil - } - var entries []triggerEntry - var walk func(p string) error - walk = func(p string) error { - dirs, err := os.ReadDir(p) - if err != nil { - return err - } - for _, entry := range dirs { - full := filepath.Join(p, entry.Name()) - if entry.IsDir() { - if err := walk(full); err != nil { - return err - } - continue - } - ext := filepath.Ext(entry.Name()) - if ext != ".yaml" && ext != ".yml" { - continue - } - id := strings.TrimSuffix(entry.Name(), ext) - // Determine kind from the parent directory name. - parent := filepath.Base(filepath.Dir(full)) - kind := "" - if parent == "global_flag" { - kind = "global" - } else if parent == "player_flag" { - kind = "player" - } - entries = append(entries, triggerEntry{ID: id, Kind: kind}) - } - return nil - } - if err := walk(root); err != nil { - return nil, err - } - return entries, nil -} - // findTriggerFile searches for a trigger file in the triggers directory tree // and returns its full path and the parent directory name (relative to the // triggers root). Searches player_flag/ then global_flag/ then root. diff --git a/internal/admin/static/triggerseditor.js b/internal/admin/static/triggerseditor.js index 7cb5d9e..0c18ce4 100644 --- a/internal/admin/static/triggerseditor.js +++ b/internal/admin/static/triggerseditor.js @@ -19,31 +19,80 @@ function initTriggerEditor() { } function loadTriggerList() { - API.get('/api/triggers').then(function(data) { - allIDs = (data || []).map(function(e) { return e.id || e; }); - window._triggerEntries = data || []; - renderTriggerList(''); - }).catch(function(e) { - notify('Failed to load list: ' + e.message, 'error'); - }); + loadList(); } function renderTriggerList(filter) { + renderList(filter); +} + +function renderList(filter) { var el = $('#listEntries'); var f = (filter || '').toLowerCase(); - var entries = window._triggerEntries || []; + var t = treeData; + + function filterIDs(ids) { + if (!f) return ids; + return ids.filter(function(id) { return String(id).toLowerCase().indexOf(f) >= 0; }); + } + + function renderItems(ids) { + var h = ''; + (ids || []).forEach(function(id) { + h += '<div class="item' + (triggerID === id ? ' active' : '') + '" onclick="loadTriggerItem(\'' + escAttr(id) + '\')" oncontextmenu="showItemContextMenu(event,\'' + escAttr(id) + '\')">' + esc(id) + '</div>'; + }); + return h; + } + var html = ''; - entries.forEach(function(entry) { - var id = entry.id || entry; - if (f && id.toLowerCase().indexOf(f) === -1) return; - var badge = entry.kind === 'player' ? ' [Player]' : (entry.kind === 'global' ? ' [Global]' : ''); - var badgeClass = entry.kind === 'player' ? 'dc-item' : (entry.kind === 'global' ? 'dc-item' : ''); - html += '<div class="item' + (triggerID === id ? ' active' : '') + '" onclick="loadTriggerItem(\'' + escAttr(id) + '\')">'; - html += '<span class="dc-id">' + esc(id) + '</span>'; - html += '<span class="dc-name" style="color:' + (entry.kind === 'player' ? '#6af' : '#e8c47c') + '">' + esc(badge) + '</span>'; - html += '</div>'; - }); + var hasDirs = Object.keys(t.dirs).length > 0; + + if (t.root.length > 0 || !hasDirs) { + if (hasDirs) { + var filtered = filterIDs(t.root); + if (filtered.length > 0) { + var open = !!f || dirOpenState['Root'] === true; + html += '<details class="dir-group" data-name="Root"' + (open ? ' open' : '') + '>'; + html += '<summary class="dir-header">'; + html += '<span class="dir-arrow"></span>'; + html += '<span class="dir-name">Root</span>'; + html += '<span class="dir-count">' + (f ? filtered.length + '/' + t.root.length : t.root.length) + '</span>'; + html += '</summary>'; + html += '<div class="dir-items">' + renderItems(filtered) + '</div>'; + html += '</details>'; + } + } else { + html += renderItems(filterIDs(t.root)); + } + } + + if (hasDirs) { + Object.keys(t.dirs).sort().forEach(function(dir) { + var ids = t.dirs[dir] || []; + if (ids.length === 0 && !f) return; + var filtered = filterIDs(ids); + if (f && filtered.length === 0) return; + + var open = !!f || dirOpenState[dir] === true; + html += '<details class="dir-group" data-name="' + escAttr(dir) + '"' + (open ? ' open' : '') + '>'; + html += '<summary class="dir-header">'; + html += '<span class="dir-arrow"></span>'; + html += '<span class="dir-name">' + esc(dir) + '</span>'; + html += '<span class="dir-count">' + (f ? filtered.length + '/' + ids.length : ids.length) + '</span>'; + html += '</summary>'; + html += '<div class="dir-items">' + renderItems(filtered) + '</div>'; + html += '</details>'; + }); + } + el.innerHTML = html || '<p style="color:#888;text-align:center;margin-top:20px;font-size:11px">No triggers</p>'; + + el.querySelectorAll('details.dir-group').forEach(function(d) { + d.addEventListener('toggle', function() { + var n = d.getAttribute('data-name'); + if (n) dirOpenState[n] = d.open; + }); + }); } function loadTriggerItem(id) { @@ -170,7 +219,8 @@ function renderTriggerEditor(id, data) { html += '<div class="btn-row">'; html += '<button class="btn btn-primary" onclick="saveTrigger()">Save</button>'; - html += '<button class="btn" style="margin-left:auto" onclick="deleteTrigger()">Remove</button>'; + html += '<button class="btn" onclick="duplicateTrigger()">Duplicate</button>'; + html += '<button class="btn btn-danger" onclick="deleteTrigger()">Delete Trigger</button>'; html += '</div>'; $('#editorMain').innerHTML = html; @@ -214,6 +264,83 @@ function saveTrigger() { }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); } +function duplicateTrigger(sourceID) { + var src = sourceID || triggerID; + if (!src) return; + + var done = function(data) { + var allTriggerIDs = (treeData.root || []).slice(); + var dirs = treeData.dirs || {}; + Object.keys(dirs).forEach(function(d) { + allTriggerIDs = allTriggerIDs.concat(dirs[d] || []); + }); + + var baseID = src + '_copy'; + var newID = baseID; + var counter = 2; + while (allTriggerIDs.indexOf(newID) >= 0) { + newID = baseID + '_' + counter; + counter++; + } + + var copy = JSON.parse(JSON.stringify(data)); + delete copy._raw; + delete copy._path; + delete copy._kind; + copy.id = newID; + ced_cleanOutput(copy); + + API.post('/api/triggers', {id: newID, on_player_flag: '', steps: []}).then(function() { + return API.put('/api/triggers/' + encodeURIComponent(newID), copy); + }).then(function() { + notify('Duplicated as ' + newID, 'success'); + updateUndoBar(); + loadTriggerList(); + loadTrigger(newID); + }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); + }; + + if (sourceID) { + API.get('/api/triggers/' + encodeURIComponent(sourceID)).then(done).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); + } else { + var flagTypeEl = document.querySelector('input[name="trigFlagType"]:checked'); + if (flagTypeEl) { + delete triggerData.on_player_flag; + delete triggerData.on_global_flag; + var n = $('#trigFlagName'); + if (flagTypeEl.value === 'player') triggerData.on_player_flag = n ? n.value.trim() : ''; + else triggerData.on_global_flag = n ? n.value.trim() : ''; + } + window._ieSyncAll(window._editorData); + mergeTriggerFromEditor(); + done(triggerData); + } +} + +function showItemContextMenu(e, id) { + var items = [ + {label: 'Duplicate', handler: function() { duplicateTrigger(id); }}, + {label: 'Delete Trigger', cls: 'danger', handler: function() { deleteTriggerByID(id); }}, + ]; + openContextMenu(e, items); +} + +function deleteTriggerByID(id) { + if (!confirm('Delete trigger "' + id + '"?')) return; + API.del('/api/triggers/' + encodeURIComponent(id)).then(function() { + notify('Trigger ' + id + ' deleted', 'success'); + updateUndoBar(); + if (triggerID === id) { + triggerID = null; + currentID = null; + triggerData = null; + triggerKind = ''; + $('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>'; + } + loadTriggerList(); + }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); +} + function deleteTrigger() { if (!confirm('Delete trigger "' + triggerID + '"?')) return; API.del('/api/triggers/' + encodeURIComponent(triggerID)).then(function() { |
