From 9292634f2f8d71a53879ff07e1330c671b207d51 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 6 Jul 2026 23:05:04 -0400 Subject: feat: split the concept of hidden and impassable exits. add player_flag for discovered hidden exits. --- internal/admin/api_map.go | 6 +++-- internal/admin/api_room_courses.go | 51 +++++++++++++++++++------------------- internal/admin/api_rooms.go | 4 +-- internal/admin/static/map.js | 21 +++++++++++++--- 4 files changed, 49 insertions(+), 33 deletions(-) (limited to 'internal/admin') 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 += ''; + return; + } if (l.hidden) { - // Hidden course-link exit: distinct color (map_course, ~#1B6FC0) and - // tighter dashed arrow style. - g += ''; + g += ''; 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 += '
'; h += '' + d + ''; h += '# ' + target + ''; + if (alwaysBlocked) h += 'Impassable'; if (bmsg) h += 'Blocked: ' + esc(bmsg) + ''; if (cond) h += 'Conditional'; + if (hidden) h += 'Hidden'; h += ''; if (isBidi) h += ''; h += ''; @@ -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 += '
'; h += '
'; h += '
'; + h += '
'; h += '
'; h += ''; @@ -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) { -- cgit v1.2.3