aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/cmd_look.go58
-rw-r--r--internal/game/cmd_toggle.go1
-rw-r--r--internal/game/game.go1
-rw-r--r--internal/game/map.go191
-rw-r--r--internal/game/map_test.go94
5 files changed, 344 insertions, 1 deletions
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index 48b7daf..0bec915 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -23,9 +23,20 @@ func (g *Game) doLook(sess *net.Session) {
sess.WriteLines(
"",
room.Name,
- room.Description,
)
+ if p.Toggles["tinymap"] && g.MapWidth > 0 {
+ descLines := wrapText(room.Description, g.MapWidth)
+ mapLines := buildTinyMap(g, p.RoomID)
+ if len(mapLines) == 7 {
+ g.writeLookSideBySide(sess, p, descLines, mapLines)
+ } else {
+ sess.WriteLine(room.Description)
+ }
+ } else {
+ sess.WriteLine(room.Description)
+ }
+
mobs := g.MobStore.MobsInRoom(p.RoomID)
if len(mobs) > 0 {
sort.Slice(mobs, func(i, j int) bool {
@@ -486,3 +497,48 @@ func mobInstanceIdx(mob *world.MobInstance, roomMobs []*world.MobInstance) int {
}
return 0
}
+
+func wrapText(text string, width int) []string {
+ if width <= 0 {
+ return []string{text}
+ }
+ words := strings.Fields(text)
+ if len(words) == 0 {
+ return nil
+ }
+ var lines []string
+ current := words[0]
+ for _, word := range words[1:] {
+ if len(current)+1+len(word) <= width {
+ current += " " + word
+ } else {
+ lines = append(lines, current)
+ current = word
+ }
+ }
+ lines = append(lines, current)
+ return lines
+}
+
+func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLines []string, mapLines []string) {
+ total := len(descLines)
+ if len(mapLines) > total {
+ total = len(mapLines)
+ }
+ leftMap := p.Toggles["left-tinymap"]
+ for i := 0; i < total; i++ {
+ desc := ""
+ if i < len(descLines) {
+ desc = descLines[i]
+ }
+ mapLine := ""
+ if i < len(mapLines) {
+ mapLine = mapLines[i]
+ }
+ if leftMap {
+ sess.WriteLine(fmt.Sprintf("%s %s", mapLine, desc))
+ } else {
+ sess.WriteLine(fmt.Sprintf("%-*s %s", g.MapWidth, desc, mapLine))
+ }
+ }
+}
diff --git a/internal/game/cmd_toggle.go b/internal/game/cmd_toggle.go
index 25b8955..a399483 100644
--- a/internal/game/cmd_toggle.go
+++ b/internal/game/cmd_toggle.go
@@ -14,6 +14,7 @@ var toggles = []struct {
}{
{"description", "Long room descriptions"},
{"tinymap", "Mini-map display"},
+ {"left-tinymap", "Mini-map on left side of descriptions"},
{"xpdrops", "XP drop messages"},
{"exits", "Long exit display in look"},
{"mobenter", "Messages when mobs enter the room"},
diff --git a/internal/game/game.go b/internal/game/game.go
index 50c8f3d..0675a13 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -28,6 +28,7 @@ type Game struct {
charsMu sync.Mutex
loggedInChars map[string]*net.Session
combatPadWidth int
+ MapWidth int
}
func New(dataDir string) *Game {
diff --git a/internal/game/map.go b/internal/game/map.go
new file mode 100644
index 0000000..cd1012b
--- /dev/null
+++ b/internal/game/map.go
@@ -0,0 +1,191 @@
+package game
+
+import (
+ "thirdcollapse/internal/world"
+)
+
+type mapGraph struct {
+ posToRoom map[[2]int]int
+ roomToPos map[int][2]int
+}
+
+var bfsDirs = []struct {
+ dir world.ExitDir
+ dx, dy int
+}{
+ {world.North, 0, -1},
+ {world.South, 0, 1},
+ {world.East, 1, 0},
+ {world.West, -1, 0},
+}
+
+func buildGraph(g *Game, startRoomID int) *mapGraph {
+ mg := &mapGraph{
+ posToRoom: make(map[[2]int]int),
+ roomToPos: make(map[int][2]int),
+ }
+
+ type node struct {
+ roomID int
+ x, y int
+ }
+ queue := []node{{startRoomID, 0, 0}}
+ mg.posToRoom[[2]int{0, 0}] = startRoomID
+ mg.roomToPos[startRoomID] = [2]int{0, 0}
+
+ for len(queue) > 0 {
+ n := queue[0]
+ queue = queue[1:]
+
+ room, ok := loadRoom(g, n.roomID)
+ if !ok {
+ continue
+ }
+
+ for _, d := range bfsDirs {
+ targetID, ok := exitTarget(room, d.dir)
+ if !ok {
+ continue
+ }
+ if _, visited := mg.roomToPos[targetID]; visited {
+ continue
+ }
+ nx, ny := n.x + d.dx, n.y + d.dy
+ mg.posToRoom[[2]int{nx, ny}] = targetID
+ mg.roomToPos[targetID] = [2]int{nx, ny}
+ queue = append(queue, node{targetID, nx, ny})
+ }
+ }
+
+ return mg
+}
+
+func buildTinyMap(g *Game, roomID int) []string {
+ mg := buildGraph(g, roomID)
+
+ grid := make([][]rune, 5)
+ for i := range grid {
+ grid[i] = make([]rune, 5)
+ for j := range grid[i] {
+ grid[i][j] = ' '
+ }
+ }
+
+ for y := -1; y <= 1; y++ {
+ for x := -1; x <= 1; x++ {
+ pos := [2]int{x, y}
+ rid, ok := mg.posToRoom[pos]
+ if !ok {
+ continue
+ }
+ gr := (y + 1) * 2
+ gc := (x + 1) * 2
+ if rid == roomID {
+ grid[gr][gc] = '@'
+ } else {
+ grid[gr][gc] = roomMapSymbol(g, rid)
+ }
+ }
+ }
+
+ for y := -1; y <= 1; y++ {
+ for x := -1; x <= 0; x++ {
+ leftPos := [2]int{x, y}
+ rightPos := [2]int{x + 1, y}
+ leftRoom, leftOK := mg.posToRoom[leftPos]
+ rightRoom, rightOK := mg.posToRoom[rightPos]
+ if !leftOK || !rightOK {
+ continue
+ }
+ if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) {
+ grid[(y+1)*2][(x+1)*2+1] = '─'
+ }
+ }
+ }
+
+ for y := -1; y <= 0; y++ {
+ for x := -1; x <= 1; x++ {
+ topPos := [2]int{x, y}
+ bottomPos := [2]int{x, y + 1}
+ topRoom, topOK := mg.posToRoom[topPos]
+ bottomRoom, bottomOK := mg.posToRoom[bottomPos]
+ if !topOK || !bottomOK {
+ continue
+ }
+ if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) {
+ grid[(y+1)*2+1][(x+1)*2] = '│'
+ }
+ }
+ }
+
+ cur, _ := loadRoom(g, roomID)
+ if cur != nil {
+ if _, ok := exitTarget(cur, world.Up); ok {
+ grid[1][3] = '↑'
+ }
+ if _, ok := exitTarget(cur, world.Down); ok {
+ grid[3][1] = '↓'
+ }
+ }
+
+ lines := make([]string, 7)
+ lines[0] = "╔═════╗"
+ for row := 0; row < 5; row++ {
+ lines[row+1] = "║" + string(grid[row]) + "║"
+ }
+ lines[6] = "╚═════╝"
+
+ return lines
+}
+
+func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool {
+ r1, ok := loadRoom(g, room1)
+ if !ok {
+ return false
+ }
+ if id, ok := exitTarget(r1, dir12); ok && id == room2 {
+ return true
+ }
+ r2, ok := loadRoom(g, room2)
+ if !ok {
+ return false
+ }
+ if id, ok := exitTarget(r2, dir21); ok && id == room1 {
+ return true
+ }
+ return false
+}
+
+func exitTarget(room *world.Room, dir world.ExitDir) (int, bool) {
+ if room == nil {
+ return 0, false
+ }
+ exit, ok := room.Exits[dir]
+ if !ok {
+ return 0, false
+ }
+ return exit.Room, true
+}
+
+func loadRoom(g *Game, roomID int) (*world.Room, bool) {
+ if roomID == 0 {
+ return nil, false
+ }
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ return room, true
+}
+
+func roomMapSymbol(g *Game, roomID int) rune {
+ room, ok := loadRoom(g, roomID)
+ if !ok {
+ return '?'
+ }
+ if room.MapSymbol != "" {
+ runes := []rune(room.MapSymbol)
+ return runes[0]
+ }
+ return 'o'
+}
diff --git a/internal/game/map_test.go b/internal/game/map_test.go
new file mode 100644
index 0000000..a50c2c2
--- /dev/null
+++ b/internal/game/map_test.go
@@ -0,0 +1,94 @@
+package game
+
+import (
+ "strings"
+ "testing"
+
+ "thirdcollapse/internal/world"
+)
+
+func TestBuildTinyMap(t *testing.T) {
+ g := &Game{
+ World: world.New("../../data"),
+ MapWidth: 70,
+ }
+
+ tests := []struct {
+ name string
+ roomID int
+ want []string // expected lines, or nil to just check count
+ }{
+ {
+ name: "room 1 has east exit",
+ roomID: 1,
+ },
+ {
+ name: "room 2 has west/north/east",
+ roomID: 2,
+ },
+ {
+ name: "room 4 has west/north/east",
+ roomID: 4,
+ },
+ {
+ name: "room 21 has west only",
+ roomID: 21,
+ },
+ {
+ name: "room 22 west of town square",
+ roomID: 22,
+ },
+ {
+ name: "room 25 east of south of west",
+ roomID: 25,
+ },
+ {
+ name: "room 8 hacking lab",
+ roomID: 8,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ lines := buildTinyMap(g, tt.roomID)
+ if len(lines) != 7 {
+ t.Fatalf("expected 7 lines, got %d", len(lines))
+ }
+ if lines[0] != "╔═════╗" {
+ t.Errorf("line 0: want ╔═════╗, got %s", lines[0])
+ }
+ if lines[6] != "╚═════╝" {
+ t.Errorf("line 6: want ╚═════╝, got %s", lines[6])
+ }
+ for i := 1; i <= 5; i++ {
+ if !strings.HasPrefix(lines[i], "║") || !strings.HasSuffix(lines[i], "║") {
+ t.Errorf("line %d: should have ║ borders, got %s", i, lines[i])
+ }
+ if len([]rune(lines[i])) != 7 {
+ t.Errorf("line %d: expected 7 runes, got %d in %q", i, len([]rune(lines[i])), lines[i])
+ }
+ }
+ t.Logf("Room %d map:\n%s", tt.roomID, strings.Join(lines, "\n"))
+ })
+ }
+}
+
+func TestWrapText(t *testing.T) {
+ tests := []struct {
+ text string
+ width int
+ want int // expected number of lines
+ }{
+ {"hello world", 70, 1},
+ {"hello world", 5, 2},
+ {"", 70, 0},
+ {"a b c d e f g h i j", 5, 4},
+ }
+
+ for _, tt := range tests {
+ result := wrapText(tt.text, tt.width)
+ if len(result) != tt.want {
+ t.Errorf("wrapText(%q, %d) = %d lines, want %d: %v", tt.text, tt.width, len(result), tt.want, result)
+ }
+ }
+}