aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-06 23:05:04 -0400
committerhistoria <[not public]>2026-07-06 23:05:04 -0400
commit9292634f2f8d71a53879ff07e1330c671b207d51 (patch)
treedcffadf7f697e8ad0705d787d8f599bdeeedb812 /internal/admin
parent911aae15f4e869c93239ef7c630c61e1d9d4ef3f (diff)
downloadthehouseoficarus-9292634f2f8d71a53879ff07e1330c671b207d51.tar.gz
feat: split the concept of hidden and impassable exits. add player_flag for discovered hidden exits.
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_map.go6
-rw-r--r--internal/admin/api_room_courses.go51
-rw-r--r--internal/admin/api_rooms.go4
-rw-r--r--internal/admin/static/map.js21
4 files changed, 49 insertions, 33 deletions
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5168165..a7802f9 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -50,7 +50,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
return nil, false
}
return room, true
- }, nil, func(gc world.GridConflict) {
+ }, nil, nil, func(gc world.GridConflict) {
if gc.Kind == "overlap" {
log.Printf("map: grid overlap: room %d wants cell %v already held by %d (via %d->%s)",
gc.Target, gc.Want, gc.Occupier, gc.From, gc.Dir)
@@ -171,6 +171,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
Dir string `json:"dir"`
Bidirectional bool `json:"bidirectional"`
Hidden bool `json:"hidden"`
+ AlwaysBlocked bool `json:"always_blocked"`
}
type UDLink struct {
@@ -260,7 +261,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
bidirectional := false
if targetRoom, ok := roomData[target]; ok {
- if oppExit, ok := targetRoom.Exits[world.OppositeExit[dir]]; ok && oppExit.Room == rid && !oppExit.Hidden {
+ if oppExit, ok := targetRoom.Exits[world.OppositeExit[dir]]; ok && oppExit.Room == rid && !oppExit.Hidden && !oppExit.AlwaysBlocked {
bidirectional = true
}
}
@@ -277,6 +278,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
Dir: string(dir),
Bidirectional: bidirectional,
Hidden: exit.Hidden,
+ AlwaysBlocked: exit.AlwaysBlocked,
})
}
}
diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go
index f81a204..86c7cdf 100644
--- a/internal/admin/api_room_courses.go
+++ b/internal/admin/api_room_courses.go
@@ -378,8 +378,8 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
NewContent: newContent,
})
- // --- hidden exit management ---
- // Create hidden exits so the map BFS lays the course rooms in sequence.
+ // --- course map exit management ---
+ // Create always-blocked exits so the map BFS lays the course rooms in sequence.
var prevRoom, nextRoom int
if insertAt > 0 {
prevRoom = obstacleRoomID(newList[insertAt-1])
@@ -391,24 +391,24 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
if prevRoom > 0 {
pm, _, _, perr := s.loadRoomMapByID(prevRoom)
if perr == nil {
- // Delete the old hidden link from prevRoom to nextRoom (now bypassed).
+ // Delete the old blocked link from prevRoom to nextRoom (now bypassed).
if nextRoom > 0 {
- oldDir := findHiddenExitDirToTarget(pm, nextRoom)
+ oldDir := findCourseExitDirToTarget(pm, nextRoom)
if oldDir != "" {
removeExit(pm, oldDir)
}
}
dir := ensureValidDirection(body.Direction, pm)
- setHiddenExit(pm, dir, roomID)
- s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("hidden exit %d->%d for course %s", prevRoom, roomID, body.CourseID))
+ setCourseBlockedExit(pm, dir, roomID)
+ s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("blocked exit %d->%d for course %s", prevRoom, roomID, body.CourseID))
}
}
if nextRoom > 0 {
cm, _, _, cerr := s.loadRoomMapByID(roomID)
if cerr == nil {
dir := autoPickDirection(cm)
- setHiddenExit(cm, dir, nextRoom)
- s.saveRoomMapByID(roomID, cm, fmt.Sprintf("hidden exit %d->%d for course %s", roomID, nextRoom, body.CourseID))
+ setCourseBlockedExit(cm, dir, nextRoom)
+ s.saveRoomMapByID(roomID, cm, fmt.Sprintf("blocked exit %d->%d for course %s", roomID, nextRoom, body.CourseID))
}
}
@@ -448,25 +448,25 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ
nextRoom = obstacleRoomID(obstacles[idx+1])
}
- // --- hidden exit cleanup ---
+ // --- course map exit cleanup ---
var relinkDir string
if prevRoom > 0 {
pm, _, _, perr := s.loadRoomMapByID(prevRoom)
if perr == nil {
- relinkDir = findHiddenExitDirToTarget(pm, roomID)
+ relinkDir = findCourseExitDirToTarget(pm, roomID)
if relinkDir != "" {
removeExit(pm, relinkDir)
- s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("remove hidden exit to %d (course %s)", roomID, id))
+ s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("remove blocked exit to %d (course %s)", roomID, id))
}
}
}
if nextRoom > 0 {
cm, _, _, cerr := s.loadRoomMapByID(roomID)
if cerr == nil {
- d := findHiddenExitDirToTarget(cm, nextRoom)
+ d := findCourseExitDirToTarget(cm, nextRoom)
if d != "" {
removeExit(cm, d)
- s.saveRoomMapByID(roomID, cm, fmt.Sprintf("remove hidden exit to %d (course %s)", nextRoom, id))
+ s.saveRoomMapByID(roomID, cm, fmt.Sprintf("remove blocked exit to %d (course %s)", nextRoom, id))
}
}
}
@@ -475,8 +475,8 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ
pm, _, _, perr := s.loadRoomMapByID(prevRoom)
if perr == nil {
dir := ensureValidDirection(relinkDir, pm)
- setHiddenExit(pm, dir, nextRoom)
- s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("relink hidden exit %d->%d (course %s)", prevRoom, nextRoom, id))
+ setCourseBlockedExit(pm, dir, nextRoom)
+ s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("relink blocked exit %d->%d (course %s)", prevRoom, nextRoom, id))
}
}
@@ -573,11 +573,12 @@ func roomExitsMap(m map[string]any) map[string]any {
return ex
}
-// setHiddenExit adds or replaces a hidden exit in direction dir targeting
-// targetRoomID.
-func setHiddenExit(m map[string]any, dir string, targetRoomID int) {
+// setCourseBlockedExit adds or replaces an always-blocked exit in direction dir
+// targeting targetRoomID. Always-blocked exits are impassable but render on the map;
+// used to lay out course rooms in visual sequence.
+func setCourseBlockedExit(m map[string]any, dir string, targetRoomID int) {
ex := roomExitsMap(m)
- ex[dir] = map[string]any{"room": targetRoomID, "hidden": true}
+ ex[dir] = map[string]any{"room": targetRoomID, "always_blocked": true}
}
// removeExit deletes an exit by direction. Returns true if it existed.
@@ -608,25 +609,25 @@ func exitTargetRoom(v any) int {
return 0
}
-// exitIsHidden returns whether an exit value carries hidden: true.
-func exitIsHidden(v any) bool {
+// exitIsAlwaysBlocked returns whether an exit value carries always_blocked: true.
+func exitIsAlwaysBlocked(v any) bool {
if mm, ok := v.(map[string]any); ok {
- if h, ok := mm["hidden"]; ok {
+ if h, ok := mm["always_blocked"]; ok {
return h == true
}
}
return false
}
-// findHiddenExitDirToTarget returns the direction of a hidden exit from m
+// findCourseExitDirToTarget returns the direction of an always-blocked exit from m
// targeting targetRoomID, or "" if none.
-func findHiddenExitDirToTarget(m map[string]any, targetRoomID int) string {
+func findCourseExitDirToTarget(m map[string]any, targetRoomID int) string {
ex, ok := m["exits"].(map[string]any)
if !ok {
return ""
}
for dir, v := range ex {
- if exitIsHidden(v) && exitTargetRoom(v) == targetRoomID {
+ if exitIsAlwaysBlocked(v) && exitTargetRoom(v) == targetRoomID {
return dir
}
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index 74143a8..75f4779 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -458,7 +458,7 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
return nil, false
}
return room, true
- }, nil, nil)
+ }, nil, nil, nil)
cA, okA := grid.Coord[body.From]
cB, okB := grid.Coord[body.To]
@@ -542,7 +542,7 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
roomCopy.Exits[oppDir] = world.ExitDef{Room: body.From}
}
return &roomCopy, true
- }, nil, func(gc world.GridConflict) {
+ }, nil, nil, func(gc world.GridConflict) {
conflicts++
})
_ = testGrid
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index 0dab98c..8080451 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -253,10 +253,12 @@ function renderMap(data) {
if (!fr || !tr) return;
var x1 = fr.x * CELL + CELL/2, y1 = fr.y * CELL + CELL/2;
var x2 = tr.x * CELL + CELL/2, y2 = tr.y * CELL + CELL/2;
+ if (l.always_blocked) {
+ g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#C44" stroke-width="5" opacity="0.75" stroke-dasharray="6,3" vector-effect="non-scaling-stroke"/>';
+ return;
+ }
if (l.hidden) {
- // Hidden course-link exit: distinct color (map_course, ~#1B6FC0) and
- // tighter dashed arrow style.
- g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#1B6FC0" stroke-width="5" opacity="0.75" stroke-dasharray="2,2" vector-effect="non-scaling-stroke"/>';
+ g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#C7C" stroke-width="5" opacity="0.7" stroke-dasharray="2,2" vector-effect="non-scaling-stroke"/>';
return;
}
var c = blendColors(fr.color, tr.color).replace('#','');
@@ -1232,15 +1234,18 @@ function renderExitsSection(r) {
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 isConditional = !!(cond || bmsg || setFlags || setPlayerFlags || hidden);
+ var alwaysBlocked = (typeof exit === 'object' && exit.always_blocked) ? exit.always_blocked : false;
+ var isConditional = !!(cond || bmsg || setFlags || setPlayerFlags || 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));
});
h += '<div class="exit-row" data-dir="' + d + '">';
h += '<span class="exit-dir"' + (isConditional ? ' style="color:#f55"' : '') + '>' + d + '</span>';
h += '<span class="exit-target" onclick="selectRoom(' + target + ')"># ' + target + '</span>';
+ if (alwaysBlocked) h += '<span class="exit-meta">Impassable</span>';
if (bmsg) h += '<span class="exit-meta">Blocked: ' + esc(bmsg) + '</span>';
if (cond) h += '<span class="exit-meta">Conditional</span>';
+ if (hidden) h += '<span class="exit-meta" style="color:#c7c">Hidden</span>';
h += '<button class="btn btn-sm" onclick="editExitCondition(\'' + d + '\')">Edit</button>';
if (isBidi) h += '<button class="btn btn-sm" onclick="deleteLink(' + target + ',' + r.id + ',\'from\')" title="Remove reverse exit to make one-way">\u21C6</button>';
h += '<button class="btn btn-sm btn-danger" onclick="deleteExit(\'' + d + '\', ' + target + ')">X</button>';
@@ -1271,6 +1276,7 @@ function editExitCondition(dir) {
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(', ');
@@ -1290,6 +1296,7 @@ function editExitCondition(dir) {
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 (impassable, shown on map)</label></div>';
h += '<div style="margin-top:10px;border-top:1px solid var(--border);padding-top:8px">';
h += '<label style="font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px">Conditions</label>';
@@ -1429,6 +1436,8 @@ function saveExitCondition() {
exit.blocked_message = (bmsg && bmsg.value) ? bmsg.value : '';
var hiddenEl = document.getElementById('exitHidden');
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 : '');
@@ -1475,6 +1484,10 @@ function clearExitCondition() {
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');
+ if (ab) ab.checked = false;
}
function parseFlagInput(val) {