aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-04 22:44:42 -0400
committerhistoria <[not public]>2026-07-04 22:44:42 -0400
commitad22ea3526a146ea8ce305c8375c35836465cc30 (patch)
tree479cec511e596bb09d3f3792dcf10a2201e9b7f8
parent7b009e904524f30cbff6346730e8d8fe4b046a6b (diff)
downloadthehouseoficarus-ad22ea3526a146ea8ce305c8375c35836465cc30.tar.gz
feat: admin gui move objects between directories and better toolbar wrapping
-rw-r--r--data/.admin_history.json1
-rw-r--r--data/mobs/flight_attendant_copy.yaml23
-rw-r--r--internal/admin/server.go43
-rw-r--r--internal/admin/static/admin.css2
-rw-r--r--internal/admin/static/dropeditor.js3
-rw-r--r--internal/admin/static/editor.js29
-rw-r--r--internal/admin/static/hazardeditor.js3
-rw-r--r--internal/admin/static/itemeditor.js3
-rw-r--r--internal/admin/static/mobeditor.js3
-rw-r--r--internal/admin/static/moduleeditor.js3
-rw-r--r--internal/admin/static/objecteditor.js3
-rw-r--r--internal/admin/static/techeditor.js3
-rw-r--r--internal/admin/yaml_util.go44
13 files changed, 129 insertions, 34 deletions
diff --git a/data/.admin_history.json b/data/.admin_history.json
deleted file mode 100644
index c68eab4..0000000
--- a/data/.admin_history.json
+++ /dev/null
@@ -1 +0,0 @@
-{"history":[{"time":"2026-07-03T20:51:13-04:00","description":"create room 2","file_path":"data/rooms/22222/2.yaml","old_content":"","new_content":"description: An empty room.\nname: New Room\n","is_delete":false,"is_create":true},{"time":"2026-07-03T20:51:15-04:00","description":"delete room 2 (cleaned 0 exits)","file_path":"data/rooms/22222/2.yaml","old_content":"description: An empty room.\nname: New Room\n","new_content":"","is_delete":true,"is_create":false}],"redo":null} \ No newline at end of file
diff --git a/data/mobs/flight_attendant_copy.yaml b/data/mobs/flight_attendant_copy.yaml
deleted file mode 100644
index a495732..0000000
--- a/data/mobs/flight_attendant_copy.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: flight attendant
-description: A smiling flight attendant in a crisp uniform, ready to assist passengers.
-unique: false
-protected: true
-idle_descriptions:
- - adjusts her name badge
- - smiles politely at you
-shop:
- message: SHOP SHOP
- buy_percentage: 0
- buys_anything: true
- change_percentage: 0
- items:
- - item_id: bronze_pickaxe
- restock_ticks: 0
- stock: 5
-talk:
- nodes:
- start:
- options:
- - goto: ""
- text: None!
- - text: None
diff --git a/internal/admin/server.go b/internal/admin/server.go
index ef95e73..b1f641a 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -117,20 +117,25 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor
apiMux.HandleFunc("/api/rooms", s.handleRooms)
apiMux.HandleFunc("/api/rooms/new-empty", s.handleCreateEmptyRoom)
apiMux.HandleFunc("/api/rooms/", s.handleRoomByID)
+ apiMux.HandleFunc("/api/objects/move", s.handleEntityMove("objects"))
apiMux.HandleFunc("/api/objects", s.handleObjects)
apiMux.HandleFunc("/api/objects/", s.handleObjectByID)
+ apiMux.HandleFunc("/api/items/move", s.handleEntityMove("items"))
apiMux.HandleFunc("/api/items", s.handleItems)
apiMux.HandleFunc("/api/items/", s.handleItemByID)
+ apiMux.HandleFunc("/api/mobs/move", s.handleEntityMove("mobs"))
apiMux.HandleFunc("/api/mobs", s.handleMobs)
apiMux.HandleFunc("/api/mobs/", s.handleMobByID)
+ apiMux.HandleFunc("/api/drops/move", s.handleEntityMove("drops"))
apiMux.HandleFunc("/api/drops", s.handleDrops)
apiMux.HandleFunc("/api/drops/", s.handleDropByID)
+ apiMux.HandleFunc("/api/hazards/move", s.handleEntityMove("hazards"))
apiMux.HandleFunc("/api/hazards", s.handleHazards)
apiMux.HandleFunc("/api/hazards/", s.handleHazardByID)
+ apiMux.HandleFunc("/api/techs/move", s.handleEntityMove("techs"))
apiMux.HandleFunc("/api/techs", s.handleTechs)
apiMux.HandleFunc("/api/techs/", s.handleTechByID)
- apiMux.HandleFunc("/api/courses", s.handleCourses)
- apiMux.HandleFunc("/api/courses/", s.handleCourseByID)
+ apiMux.HandleFunc("/api/modules/move", s.handleEntityMove("modules"))
apiMux.HandleFunc("/api/modules", s.handleModules)
apiMux.HandleFunc("/api/modules/", s.handleModuleByID)
apiMux.HandleFunc("/api/players", s.handlePlayers)
@@ -461,6 +466,40 @@ func (s *AdminServer) doRedo(w http.ResponseWriter, r *http.Request) {
writeJSON(w, resp)
}
+func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ var body struct {
+ ID string `json:"id"`
+ Dir string `json:"dir"`
+ }
+ if err := readJSON(r, &body); err != nil || body.ID == "" {
+ writeJSON(w, map[string]any{"error": "invalid"})
+ return
+ }
+ oldPath, newPath, data, err := moveEntityFile(s.dataDir, entityType, body.ID, body.Dir)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": err.Error()})
+ return
+ }
+ desc := fmt.Sprintf("move %s %s to %s/", entityType, body.ID, body.Dir)
+ if body.Dir == "" {
+ desc = fmt.Sprintf("move %s %s to root", entityType, body.ID)
+ }
+ s.undoStack.Push(ChangeDesc{
+ Description: desc,
+ FilePath: oldPath,
+ NewFilePath: newPath,
+ OldContent: data,
+ NewContent: data,
+ })
+ writeJSON(w, map[string]any{"ok": true})
+ }
+}
+
func (s *AdminServer) rebuildAfterUndo(change *ChangeDesc) {
s.world.RebuildRoomIndex(s.dataDir)
s.world.ClearHazardCache()
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index f068207..42e64fa 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -42,7 +42,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100
.login{max-width:360px;margin:80px auto;background:var(--panel);padding:30px;border:1px solid var(--border);border-radius:6px}
.login h1{font-size:18px;margin-bottom:20px;text-align:center}
.z-controls-wrapper{position:absolute;top:12px;left:12px;z-index:10}
-.z-controls{position:relative;display:flex;gap:6px;align-items:center;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border)}
+.z-controls{position:relative;display:flex;gap:6px;align-items:center;flex-wrap:wrap;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border)}
.z-controls button{width:28px;height:28px;font-size:16px;line-height:1;background:var(--input-bg);color:var(--text);border:1px solid var(--border);border-radius:3px;cursor:pointer}
.z-controls button:hover{background:var(--accent)}
.z-controls .z-label{font-size:13px;min-width:40px;text-align:center;font-weight:bold}
diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js
index 1569a04..dddcddb 100644
--- a/internal/admin/static/dropeditor.js
+++ b/internal/admin/static/dropeditor.js
@@ -66,6 +66,7 @@ function renderDropEditor(id, data) {
html += renderCard(sec, data);
});
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -93,8 +94,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
h += '</div>';
if (!collapsed) {
h += '<div class="obj-card-body">';
diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js
index 37bc797..35bdbe6 100644
--- a/internal/admin/static/editor.js
+++ b/internal/admin/static/editor.js
@@ -233,3 +233,32 @@ function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt
function escAttr(s) { return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;'); }
window.refreshPage = function() { loadList(); if (currentID) loadItem(currentID); };
+
+function renderMoveBar() {
+ var dirs = Object.keys(treeData.dirs).sort();
+ var html = '<div class="add-section-bar">';
+ html += '<select id="moveDirSelect">';
+ html += '<option value="">Move to directory...</option>';
+ html += '<option value="">Root</option>';
+ dirs.forEach(function(dir) {
+ html += '<option value="' + escAttr(dir) + '">' + esc(dir) + '</option>';
+ });
+ html += '</select>';
+ html += '<button class="btn btn-primary btn-sm" onclick="moveToDirectory()">Move</button>';
+ html += '</div>';
+ return html;
+}
+
+function moveToDirectory() {
+ var sel = $('#moveDirSelect');
+ var idx = sel.selectedIndex;
+ if (idx === 0) return;
+ var dir = sel.value;
+ if (!confirm('Move ' + currentID + ' to ' + (dir || 'Root') + '?')) return;
+ API.post('/api/' + editorType + '/move', {id: currentID, dir: dir}).then(function() {
+ notify('Moved ' + currentID + ' to ' + (dir || 'Root'), 'success');
+ updateUndoBar();
+ loadList();
+ loadItem(currentID);
+ }).catch(function(e) { notify('Move failed: ' + e.message, 'error'); });
+}
diff --git a/internal/admin/static/hazardeditor.js b/internal/admin/static/hazardeditor.js
index b07456e..ad317cc 100644
--- a/internal/admin/static/hazardeditor.js
+++ b/internal/admin/static/hazardeditor.js
@@ -90,6 +90,7 @@ function renderHazardEditor(id, data) {
html += '</select>';
html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>';
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -117,8 +118,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
if (!sec.always) {
h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>';
}
diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js
index 641f064..ce28581 100644
--- a/internal/admin/static/itemeditor.js
+++ b/internal/admin/static/itemeditor.js
@@ -200,6 +200,7 @@ function renderItemEditor(id, data) {
html += '</select>';
html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>';
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -237,8 +238,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
if (!sec.always) {
h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>';
}
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js
index 4260e49..802b48a 100644
--- a/internal/admin/static/mobeditor.js
+++ b/internal/admin/static/mobeditor.js
@@ -175,6 +175,7 @@ function renderMobEditor(id, data) {
html += '<button class="btn btn-primary btn-sm" onclick="mobAddSection()">Add</button>';
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -212,8 +213,8 @@ function renderMobCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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>';
+ h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleMobCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
if (!sec.always) {
h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeMobSection(\'' + sec.id + '\')">X</button>';
}
diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js
index 128148d..1e7de30 100644
--- a/internal/admin/static/moduleeditor.js
+++ b/internal/admin/static/moduleeditor.js
@@ -91,6 +91,7 @@ function renderModuleEditor(id, data) {
html += '</select>';
html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>';
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -118,8 +119,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
if (!sec.always) {
h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>';
}
diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js
index 3cbb37a..c51628f 100644
--- a/internal/admin/static/objecteditor.js
+++ b/internal/admin/static/objecteditor.js
@@ -177,6 +177,7 @@ function renderObjectEditor(id, data) {
html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>';
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -226,8 +227,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
if (!sec.always) {
h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>';
}
diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js
index 3d74254..4849fb1 100644
--- a/internal/admin/static/techeditor.js
+++ b/internal/admin/static/techeditor.js
@@ -80,6 +80,7 @@ function renderTechEditor(id, data) {
html += renderCard(sec, data);
});
html += '</div>';
+ html += renderMoveBar();
html += '</div>';
html += '<div class="tab-content" id="tabYaml" style="display:none">';
@@ -107,8 +108,8 @@ function renderCard(sec, data) {
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 ? '&#9654;' : '&#9660;') + '</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 += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>';
h += '</div>';
if (!collapsed) {
h += '<div class="obj-card-body">';
diff --git a/internal/admin/yaml_util.go b/internal/admin/yaml_util.go
index 4d0a814..465be38 100644
--- a/internal/admin/yaml_util.go
+++ b/internal/admin/yaml_util.go
@@ -2,6 +2,7 @@ package admin
import (
"bytes"
+ "fmt"
"log"
"os"
"path/filepath"
@@ -154,3 +155,46 @@ func backupFile(path string) []byte {
func snapshotFile(path string) ([]byte, error) {
return os.ReadFile(path)
}
+
+func findEntityPath(dataDir, subdir, id string) (string, error) {
+ _, path, err := readYAMLFile(dataDir, subdir, id)
+ if err == nil {
+ return path, nil
+ }
+ _, path, err = findYAMLFileInSubdirs(dataDir, subdir, id)
+ return path, err
+}
+
+func moveEntityFile(dataDir, subdir, id, targetDir string) (string, string, []byte, error) {
+ oldPath, err := findEntityPath(dataDir, subdir, id)
+ if err != nil {
+ return "", "", nil, fmt.Errorf("entity not found: %s/%s", subdir, id)
+ }
+ data, err := os.ReadFile(oldPath)
+ if err != nil {
+ return "", "", nil, fmt.Errorf("read: %w", err)
+ }
+ var newPath string
+ if targetDir == "" {
+ newPath = filepath.Join(dataDir, subdir, id+".yaml")
+ } else {
+ destDir := filepath.Join(dataDir, subdir, targetDir)
+ if _, err := os.Stat(destDir); os.IsNotExist(err) {
+ return "", "", nil, fmt.Errorf("directory does not exist: %s", targetDir)
+ }
+ newPath = filepath.Join(destDir, id+".yaml")
+ }
+ if oldPath == newPath {
+ return "", "", nil, fmt.Errorf("already in target directory")
+ }
+ if _, err := os.Stat(newPath); err == nil {
+ return "", "", nil, fmt.Errorf("entity already exists in target directory")
+ }
+ if err := os.WriteFile(newPath, data, 0644); err != nil {
+ return "", "", nil, fmt.Errorf("write: %w", err)
+ }
+ if err := os.Remove(oldPath); err != nil {
+ return "", "", nil, fmt.Errorf("remove old: %w", err)
+ }
+ return oldPath, newPath, data, nil
+}