aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_room_insert_remove_test.go101
-rw-r--r--internal/admin/api_rooms.go4
-rw-r--r--internal/admin/static/intereditor.js19
-rw-r--r--internal/admin/static/map.js25
4 files changed, 56 insertions, 93 deletions
diff --git a/internal/admin/api_room_insert_remove_test.go b/internal/admin/api_room_insert_remove_test.go
index 5a3b28e..6d80595 100644
--- a/internal/admin/api_room_insert_remove_test.go
+++ b/internal/admin/api_room_insert_remove_test.go
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
+ "thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/world"
)
@@ -93,8 +94,8 @@ func postRemove(t *testing.T, s *AdminServer, body string) (map[string]any, int)
// rewires A and B onto it, and pushes an undo entry that restores everything.
func TestInsertSuccess(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n west: 1\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n west: {room: 1}\n",
})
resp, code := postInsert(t, s, `{"from":1,"dir":"east","name":"Mid"}`)
@@ -157,8 +158,8 @@ func TestInsertSuccess(t *testing.T) {
// TestInsertDefaultName confirms an empty name falls back to "Room #<id>".
func TestInsertDefaultName(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n west: 1\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n west: {room: 1}\n",
})
resp, code := postInsert(t, s, `{"from":1,"dir":"east","name":""}`)
if code != http.StatusOK {
@@ -177,14 +178,14 @@ func TestInsertGridConflict(t *testing.T) {
// 1→east→2→east→3 and 1→south→4→east→5→east→6→east→7→north→8 at (3,0,0).
// Inserting between 1 and 2 pushes 3 to (3,0,0), colliding with 8.
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n south: 4\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n west: 2\n",
- 4: "name: D\nexits:\n east: 5\n north: 1\n",
- 5: "name: E\nexits:\n east: 6\n west: 4\n",
- 6: "name: F\nexits:\n east: 7\n west: 5\n",
- 7: "name: G\nexits:\n north: 8\n west: 6\n",
- 8: "name: H\nexits:\n south: 7\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n south: {room: 4}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
+ 4: "name: D\nexits:\n east: {room: 5}\n north: {room: 1}\n",
+ 5: "name: E\nexits:\n east: {room: 6}\n west: {room: 4}\n",
+ 6: "name: F\nexits:\n east: {room: 7}\n west: {room: 5}\n",
+ 7: "name: G\nexits:\n north: {room: 8}\n west: {room: 6}\n",
+ 8: "name: H\nexits:\n south: {room: 7}\n",
})
resp, code := postInsert(t, s, `{"from":1,"dir":"east","name":"X"}`)
if code != http.StatusConflict {
@@ -210,9 +211,9 @@ func TestInsertGridConflict(t *testing.T) {
// undo/redo round-trip.
func TestRemoveSuccess(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n west: 2\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
})
resp, code := postRemove(t, s, `{"from":1,"dir":"east"}`)
if code != http.StatusOK {
@@ -282,8 +283,8 @@ func TestRemoveGuards(t *testing.T) {
{
name: "no such exit",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n west: 1\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n west: {room: 1}\n",
},
body: `{"from":1,"dir":"north"}`,
want: "has no north exit",
@@ -291,7 +292,7 @@ func TestRemoveGuards(t *testing.T) {
{
name: "self loop",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 1\n",
+ 1: "name: A\nexits:\n east: {room: 1}\n",
},
body: `{"from":1,"dir":"east"}`,
want: "loops back",
@@ -299,9 +300,9 @@ func TestRemoveGuards(t *testing.T) {
{
name: "not an insert chain (B does not lead back)",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n",
- 3: "name: C\nexits:\n west: 2\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
},
body: `{"from":1,"dir":"east"}`,
want: "not an insert chain",
@@ -309,8 +310,8 @@ func TestRemoveGuards(t *testing.T) {
{
name: "dead end (B has no forward exit)",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n west: 1\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n west: {room: 1}\n",
},
body: `{"from":1,"dir":"east"}`,
want: "no room beyond",
@@ -318,10 +319,10 @@ func TestRemoveGuards(t *testing.T) {
{
name: "B has extra exits",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n north: 4\n",
- 3: "name: C\nexits:\n west: 2\n",
- 4: "name: D\nexits:\n south: 2\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n north: {room: 4}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
+ 4: "name: D\nexits:\n south: {room: 2}\n",
},
body: `{"from":1,"dir":"east"}`,
want: "other exits besides",
@@ -329,10 +330,10 @@ func TestRemoveGuards(t *testing.T) {
{
name: "C does not lead back to B",
rooms: map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n east: 4\n",
- 4: "name: D\nexits:\n west: 3\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n east: {room: 4}\n",
+ 4: "name: D\nexits:\n west: {room: 3}\n",
},
body: `{"from":1,"dir":"east"}`,
want: "does not lead back to",
@@ -363,12 +364,12 @@ func TestRemoveGridConflict(t *testing.T) {
// to (1,0,0); 6 (at 3's old south neighbor (2,1,0)) follows to (1,1,0)? No
// — the pull shifts the whole beyond-component; 6 lands on 5's cell (1,1,0).
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n south: 4\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n south: 6\n west: 2\n",
- 4: "name: D\nexits:\n east: 5\n north: 1\n",
- 5: "name: E\nexits:\n west: 4\n",
- 6: "name: F\nexits:\n north: 3\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n south: {room: 4}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n south: {room: 6}\n west: {room: 2}\n",
+ 4: "name: D\nexits:\n east: {room: 5}\n north: {room: 1}\n",
+ 5: "name: E\nexits:\n west: {room: 4}\n",
+ 6: "name: F\nexits:\n north: {room: 3}\n",
})
resp, code := postRemove(t, s, `{"from":1,"dir":"east"}`)
if code != http.StatusConflict {
@@ -394,10 +395,10 @@ func TestRemoveGridConflict(t *testing.T) {
// room points into B, and that the message names the count.
func TestRemoveExtraInbound(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n west: 2\n",
- 9: "name: X\nexits:\n north: 2\n", // extra inbound edge into B
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
+ 9: "name: X\nexits:\n north: {room: 2}\n", // extra inbound edge into B
})
resp, code := postRemove(t, s, `{"from":1,"dir":"east"}`)
if code != http.StatusBadRequest {
@@ -418,12 +419,12 @@ func TestNextRoomIDNoCrossSubdirCollision(t *testing.T) {
t.Fatal(err)
}
// Room 1 in root, room 2 in a subdirectory "zone".
- writeRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRoomFile(t, dir, 1, "name: A\nexits:\n east: {room: 2}\n")
zoneDir := filepath.Join(roomsDir, "zone")
if err := os.MkdirAll(zoneDir, 0o755); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(filepath.Join(zoneDir, "2.yaml"), []byte("name: B\nexits:\n west: 1\n"), 0o644); err != nil {
+ if err := os.WriteFile(filepath.Join(zoneDir, "2.yaml"), []byte("name: B\nexits:\n west: {room: 1}\n"), 0o644); err != nil {
t.Fatal(err)
}
s := &AdminServer{
@@ -461,8 +462,8 @@ func testMobDef() *world.MobDef {
// instances for the (now-deleted) new room, via rebuildAfterUndo's stat check.
func TestUndoInsertClearsMobs(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n west: 1\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n west: {room: 1}\n",
})
resp, code := postInsert(t, s, `{"from":1,"dir":"east","name":"Mid"}`)
if code != http.StatusOK {
@@ -471,7 +472,7 @@ func TestUndoInsertClearsMobs(t *testing.T) {
newID := int(resp["room"].(map[string]any)["id"].(float64))
// Spawn a transient mob in the new room so the store has something to clear.
- inst := s.mobStore.SpawnTransient(testMobDef(), &world.SpawnMobConfig{ID: "testmob"}, newID, "")
+ inst := s.mobStore.SpawnTransient(testMobDef(), &behavior.SpawnMobConfig{ID: "testmob"}, newID, "")
if inst == nil {
t.Fatal("SpawnTransient returned nil")
}
@@ -494,9 +495,9 @@ func TestUndoInsertClearsMobs(t *testing.T) {
// for the re-deleted room.
func TestRedoRemoveClearsMobs(t *testing.T) {
s := newTestAdminServer(t, map[int]string{
- 1: "name: A\nexits:\n east: 2\n",
- 2: "name: B\nexits:\n east: 3\n west: 1\n",
- 3: "name: C\nexits:\n west: 2\n",
+ 1: "name: A\nexits:\n east: {room: 2}\n",
+ 2: "name: B\nexits:\n east: {room: 3}\n west: {room: 1}\n",
+ 3: "name: C\nexits:\n west: {room: 2}\n",
})
if _, code := postRemove(t, s, `{"from":1,"dir":"east"}`); code != http.StatusOK {
t.Fatal("remove failed")
@@ -510,7 +511,7 @@ func TestRedoRemoveClearsMobs(t *testing.T) {
if !roomExists(s, 2) {
t.Fatal("precondition: B should exist after undo")
}
- s.mobStore.SpawnTransient(testMobDef(), &world.SpawnMobConfig{ID: "testmob"}, 2, "")
+ s.mobStore.SpawnTransient(testMobDef(), &behavior.SpawnMobConfig{ID: "testmob"}, 2, "")
if got := len(s.mobStore.MobsInRoom(2)); got != 1 {
t.Fatalf("precondition: expected 1 mob in B, got %d", got)
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index 0925f8b..1339bcd 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -171,7 +171,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
}
if !linkOneWay {
- exits[string(opp)] = *linkFrom
+ exits[string(opp)] = map[string]any{"room": *linkFrom}
}
}
}
@@ -1081,7 +1081,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
roomName := fmt.Sprintf("Room #%d", id)
m := map[string]any{
"name": roomName,
- "description": "An empty room.",
+ "description": []map[string]any{{"text": "An empty room."}},
}
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js
index 4e48df1..b46c456 100644
--- a/internal/admin/static/intereditor.js
+++ b/internal/admin/static/intereditor.js
@@ -944,23 +944,6 @@ function ie_syncActEntry(ed, secId, secPath, idx) {
delete ed[secPath][idx].message;
}
-// ── Upgrade: migrate legacy message/condition/action on load ──
-
-function ie_upgradeItem(item) {
- if (!item) return;
- if (item.message && typeof item.message === 'string' && item.message.trim()) {
- if (!item.action || typeof item.action !== 'object') item.action = {};
- if (!item.action.message) item.action.message = item.message;
- delete item.message;
- }
- if (item.condition && typeof item.condition === 'string') {
- try { item.condition = JSON.parse(item.condition); } catch(e) { delete item.condition; }
- }
- if (item.action && typeof item.action === 'string') {
- try { item.action = JSON.parse(item.action); } catch(e) { delete item.action; }
- }
-}
-
// ── Shared interaction-style array-section renderer ──
//
// Replaces the duplicated `renderArraySection` (objecteditor.js) and
@@ -984,8 +967,6 @@ function ie_renderArraySection(ctx) {
var h = '';
arr.forEach(function(item, idx) {
- ie_upgradeItem(item);
-
visMap[idx] = visMap[idx] || {};
var vis = visMap[idx];
var showItem = vis.item_id || (item.item_id && item.item_id !== '');
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index 3507e2d..aeb2d57 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -1433,11 +1433,9 @@ function renderExitsSection(r) {
var target = typeof exit === 'object' ? exit.room : exit;
var cond = (typeof exit === 'object' && exit.condition) ? exit.condition : null;
var bmsg = (typeof exit === 'object' && exit.blocked_message) ? exit.blocked_message : '';
- var setFlags = (typeof exit === 'object' && exit.set_flags) ? exit.set_flags : null;
- var setPlayerFlags = (typeof exit === 'object' && exit.set_player_flags) ? exit.set_player_flags : null;
var hidden = (typeof exit === 'object' && exit.hidden) ? exit.hidden : false;
var alwaysBlocked = (typeof exit === 'object' && exit.always_blocked) ? exit.always_blocked : false;
- var isConditional = !!(cond || bmsg || setFlags || setPlayerFlags || hidden || alwaysBlocked);
+ var isConditional = !!(cond || bmsg || hidden || alwaysBlocked);
var isBidi = mapData && mapData.links && mapData.links.some(function(l) {
return l.bidirectional && ((l.from === r.id && l.to === target) || (l.from === target && l.to === r.id));
});
@@ -1475,16 +1473,9 @@ function editExitCondition(dir) {
if (typeof exit !== 'object') { exit = {room: exit}; }
var cond = exit.condition || null;
var bmsg = exit.blocked_message || '';
- var setFlags = exit.set_flags || null;
- var setPlayerFlags = exit.set_player_flags || null;
var hidden = exit.hidden || false;
var alwaysBlocked = exit.always_blocked || false;
- var flagsStr = '';
- if (setFlags) flagsStr = Object.keys(setFlags).map(function(k) { return k + '=' + setFlags[k]; }).join(', ');
- var pflagsStr = '';
- if (setPlayerFlags) pflagsStr = Object.keys(setPlayerFlags).map(function(k) { return k + '=' + setPlayerFlags[k]; }).join(', ');
-
var parsed = parseCondition(cond);
var overlay = document.createElement('div');
@@ -1494,8 +1485,6 @@ function editExitCondition(dir) {
menu.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);min-width:520px;max-width:90vw;max-height:85vh;overflow-y:auto;overflow-x:hidden;padding:14px;z-index:201';
var h = '<h3 style="margin-bottom:8px;font-size:13px">Exit: ' + esc(dir) + ' &rarr; #' + exit.room + '</h3>';
- h += '<div class="form-group"><label>Set Flags (key=value, comma separated)</label><input id="exitFlags" value="' + escAttr(flagsStr) + '"></div>';
- h += '<div class="form-group"><label>Set Player Flags (key=value, comma separated)</label><input id="exitPFlags" value="' + escAttr(pflagsStr) + '"></div>';
h += '<div class="form-group"><label>Blocked Message</label><textarea id="exitBmsg" rows="2">' + esc(bmsg) + '</textarea></div>';
h += '<div class="form-group"><label><input type="checkbox" id="exitHidden"' + (hidden ? ' checked' : '') + '> Hidden</label></div>';
h += '<div class="form-group"><label><input type="checkbox" id="exitAlwaysBlocked"' + (alwaysBlocked ? ' checked' : '') + '> Always Blocked (blocked, shown on map)</label></div>';
@@ -1540,7 +1529,7 @@ function parseCondition(cond) {
function extractClause(c) {
var not = c.not === true;
if (c.player_flag && c.player_flag !== '') return {type: 'player_flag', value: c.player_flag, not: not};
- if (c.flag && c.flag !== '') return {type: 'flag', value: c.flag, not: not};
+ if (c.global_flag && c.global_flag !== '') return {type: 'global_flag', value: c.global_flag, not: not};
if (c.has_item && c.has_item !== '') return {type: 'has_item', value: c.has_item, not: not};
if (c.min_credits && c.min_credits !== 0) return {type: 'min_credits', value: String(c.min_credits), not: not};
if (c.value && typeof c.value === 'object' && c.value.key) return {type: 'value', value: c.value.key + '=' + (c.value.value || ''), not: not};
@@ -1555,7 +1544,7 @@ function renderConditionClauses(menu) {
clauses.forEach(function(clause, i) {
h += '<div class="cond-clause" data-idx="' + i + '" style="display:flex;gap:4px;align-items:center;margin-bottom:4px">';
h += '<select class="cond-type" onchange="var c=this.parentElement;var m=this.closest(\'.exit-modal\');var idx=parseInt(c.getAttribute(\'data-idx\'));m._condClauses[idx].type=this.value;renderConditionClauses(m)">';
- ['player_flag','flag','value','has_item','min_credits'].forEach(function(t) {
+ ['player_flag','global_flag','value','has_item','min_credits'].forEach(function(t) {
h += '<option value="' + t + '"' + (clause.type === t ? ' selected' : '') + '>' + t + '</option>';
});
h += '</select>';
@@ -1640,10 +1629,6 @@ function saveExitCondition() {
exit.hidden = hiddenEl ? hiddenEl.checked : false;
var alwaysBlockedEl = document.getElementById('exitAlwaysBlocked');
exit.always_blocked = alwaysBlockedEl ? alwaysBlockedEl.checked : false;
- var flagsStr = document.getElementById('exitFlags');
- var pflagsStr = document.getElementById('exitPFlags');
- exit.set_flags = parseFlagInput(flagsStr ? flagsStr.value : '');
- exit.set_player_flags = parseFlagInput(pflagsStr ? pflagsStr.value : '');
var clauseEls = menu.querySelectorAll('.cond-clause');
clauseEls.forEach(function(el) { updateClauseData(el); });
@@ -1682,10 +1667,6 @@ function clearExitCondition() {
renderConditionClauses(menu);
var bmsg = document.getElementById('exitBmsg');
if (bmsg) bmsg.value = '';
- var ef = document.getElementById('exitFlags');
- if (ef) ef.value = '';
- var pf = document.getElementById('exitPFlags');
- if (pf) pf.value = '';
var hx = document.getElementById('exitHidden');
if (hx) hx.checked = false;
var ab = document.getElementById('exitAlwaysBlocked');