diff options
Diffstat (limited to 'internal/admin')
| -rw-r--r-- | internal/admin/api_room_insert_remove_test.go | 101 | ||||
| -rw-r--r-- | internal/admin/api_rooms.go | 4 | ||||
| -rw-r--r-- | internal/admin/static/intereditor.js | 19 | ||||
| -rw-r--r-- | internal/admin/static/map.js | 25 |
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) + ' → #' + 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'); |
