From 5b9a75a1520a198c9c6b1f1f55e05c9f4aab5629 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 22 Jun 2026 04:24:11 -0400 Subject: feat: map now only reveals with exploration. players can set custom map symbols. --- internal/game/cmd_look.go | 579 ++++++++++++++++++++++++---------------------- 1 file changed, 298 insertions(+), 281 deletions(-) (limited to 'internal/game/cmd_look.go') diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 903b32d..98d48fd 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -23,10 +23,29 @@ func (g *Game) doLook(sess *net.Session) { } g.showRoomName(sess, p, room) - g.showRoomDescription(sess, p, room) - g.showRoomObjects(sess, p, room) - g.showRoomMobs(sess, p, room) - g.showGroundItems(sess, p, room) + + var mapLines []string + if p.OptionString("tiny_map") != "off" { + mapLines = buildTinyMap(g, sess, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) + if len(mapLines) != 7 { + mapLines = nil + } + } + + var content []string + content = append(content, g.showRoomDescription(sess, p, room)...) + content = append(content, g.showRoomObjects(sess, p, room)...) + content = append(content, g.showRoomMobs(sess, p, room)...) + content = append(content, g.showGroundItems(sess, p, room)...) + + if len(mapLines) == 7 { + g.writeLookSideBySide(sess, p, content, mapLines) + } else { + for _, line := range content { + sess.WriteLine(line) + } + } + g.showRoomExits(sess, p, room) g.showRoomPlayers(sess, p, room) } @@ -38,7 +57,7 @@ func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Roo ) } -func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) []string { descWidth := p.OptionInt("room_desc_width") if descWidth <= 0 { descWidth = 70 @@ -46,319 +65,316 @@ func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *wo rawLines := wrapText(room.Description, descWidth) mode := g.colorMode(sess) roomDescSpec := g.resolveColor(sess, "room_desc") - descLines := make([]string, len(rawLines)) + lines := make([]string, len(rawLines)) for i, l := range rawLines { - descLines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) - } - wroteDesc := false - if p.OptionString("tiny_map") != "off" { - mapLines := buildTinyMap(g, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) - if len(mapLines) == 7 { - g.writeLookSideBySide(sess, p, descLines, mapLines) - wroteDesc = true - } - } - if !wroteDesc { - for _, line := range descLines { - sess.WriteLine(line) - } + lines[i] = color.ExpandTagsDefault(mode, roomDescSpec, l) } + return lines } -func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string { mobs := g.MobStore.MobsInRoom(p.RoomID) - if len(mobs) > 0 { - sort.Slice(mobs, func(i, j int) bool { - iDamaged := mobs[i].HP < mobs[i].MaxHP - jDamaged := mobs[j].HP < mobs[j].MaxHP - if iDamaged != jDamaged { - return iDamaged - } - return mobs[i].InstanceID < mobs[j].InstanceID - }) - sess.WriteLine("") - playerLevel := p.CombatLevel() - for _, m := range mobs { - hp := "" - if m.HP < m.MaxHP { - hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) - } - var desc string - if combat.IsMobInCombat(m.InstanceID) { - def, err := g.MobStore.LoadDef(m.DefID) - if err == nil && len(def.CombatDescriptions) > 0 { - target := combat.GetMobTarget(m.InstanceID) - pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] - desc = " " + fmt.Sprintf(pattern, target) - } - } else if m.IdleDescription != "" { - desc = fmt.Sprintf(" %s", m.IdleDescription) - } - displayName := m.Name - if !m.Unique { - displayName = "A " + m.Name - } - mobColor := "mob" - if m.Protected { - mobColor = "protected_mob" - } - mobLevel := mobCombatLevel(m) - levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) - sess.WriteLine(fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) + if len(mobs) == 0 { + return nil + } + sort.Slice(mobs, func(i, j int) bool { + iDamaged := mobs[i].HP < mobs[i].MaxHP + jDamaged := mobs[j].HP < mobs[j].MaxHP + if iDamaged != jDamaged { + return iDamaged + } + return mobs[i].InstanceID < mobs[j].InstanceID + }) + var lines []string + lines = append(lines, "") + playerLevel := p.CombatLevel() + for _, m := range mobs { + hp := "" + if m.HP < m.MaxHP { + hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) + } + var desc string + if combat.IsMobInCombat(m.InstanceID) { + def, err := g.MobStore.LoadDef(m.DefID) + if err == nil && len(def.CombatDescriptions) > 0 { + target := combat.GetMobTarget(m.InstanceID) + pattern := def.CombatDescriptions[randInt(len(def.CombatDescriptions))] + desc = " " + fmt.Sprintf(pattern, target) + } + } else if m.IdleDescription != "" { + desc = fmt.Sprintf(" %s", m.IdleDescription) + } + displayName := m.Name + if !m.Unique { + displayName = "A " + m.Name + } + mobColor := "mob" + if m.Protected { + mobColor = "protected_mob" } + mobLevel := mobCombatLevel(m) + levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) + lines = append(lines, fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) } + return lines } -func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) []string { objs := g.World.AllObjInstances(p.RoomID) - if len(objs) > 0 { - sess.WriteLine("") - grouped := make(map[string]int) - var order []string - for _, o := range objs { - if _, ok := grouped[o.DefID]; !ok { - order = append(order, o.DefID) - } - grouped[o.DefID]++ + if len(objs) == 0 { + return g.showFarmPatches(sess, p) + } + var lines []string + lines = append(lines, "") + grouped := make(map[string]int) + var order []string + for _, o := range objs { + if _, ok := grouped[o.DefID]; !ok { + order = append(order, o.DefID) + } + grouped[o.DefID]++ + } + for _, objID := range order { + count := grouped[objID] + def, err := g.ObjectStore.Load(objID) + if err != nil { + continue } - for _, objID := range order { - count := grouped[objID] - def, err := g.ObjectStore.Load(objID) - if err != nil { - continue - } - if def.Hidden { - continue - } + if def.Hidden { + continue + } + + if farmPatchDefIDs[objID] { + continue + } - if farmPatchDefIDs[objID] { + type instInfo struct { + idx int + depleted bool + sharedMax int + sharedCur int + respawnIn int + quality int + } + var instances []instInfo + for i := 0; i < count; i++ { + st := g.World.GetObjState(p.RoomID, objID, i) + if st == nil { continue } + instances = append(instances, instInfo{ + idx: i + 1, + depleted: st.Depleted, + sharedMax: int(st.SharedMax), + sharedCur: st.SharedTimer, + respawnIn: int(st.DepleteTimer), + quality: int(st.Quality), + }) + } + multi := len(instances) > 1 + showTimers := p.OptionBool("depletion") + + var freshIdxs []int + var timed, depleted []instInfo + for _, ins := range instances { + if ins.depleted { + depleted = append(depleted, ins) + } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { + timed = append(timed, ins) + } else { + freshIdxs = append(freshIdxs, ins.idx) + } + } - type instInfo struct { - idx int - depleted bool - sharedMax int - sharedCur int - respawnIn int - quality int + roomDesc := def.InRoomDescription + if roomDesc != "" { + roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) + } + coloredName := g.objColorize(sess, def, def.Name) + coloredPlural := g.objColorize(sess, def, def.Name+"s") + + if len(freshIdxs) > 0 { + var line string + if roomDesc != "" { + line = roomDesc + } else if len(freshIdxs) == 1 { + line = fmt.Sprintf("A %s is here.", coloredName) + } else { + line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) } - var instances []instInfo - for i := 0; i < count; i++ { - st := g.World.GetObjState(p.RoomID, objID, i) - if st == nil { - continue - } - instances = append(instances, instInfo{ - idx: i + 1, - depleted: st.Depleted, - sharedMax: int(st.SharedMax), - sharedCur: st.SharedTimer, - respawnIn: int(st.DepleteTimer), - quality: int(st.Quality), - }) + var suffix string + if multi && (len(timed) > 0 || len(depleted) > 0) { + suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) } - multi := len(instances) > 1 - showTimers := p.OptionBool("depletion") - - var freshIdxs []int - var timed, depleted []instInfo - for _, ins := range instances { - if ins.depleted { - depleted = append(depleted, ins) - } else if ins.sharedMax > 0 && ins.sharedCur < ins.sharedMax { - timed = append(timed, ins) - } else { - freshIdxs = append(freshIdxs, ins.idx) - } + qualityTimer := "" + if showTimers && len(instances) > 0 && instances[0].quality > 0 { + qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) } + lines = append(lines, fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) + } - roomDesc := def.InRoomDescription + for _, ins := range timed { + var line string if roomDesc != "" { - roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc) + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) } - coloredName := g.objColorize(sess, def, def.Name) - coloredPlural := g.objColorize(sess, def, def.Name+"s") - - if len(freshIdxs) > 0 { - var line string - if roomDesc != "" { - line = roomDesc - } else if len(freshIdxs) == 1 { - line = fmt.Sprintf("A %s is here.", coloredName) - } else { - line = fmt.Sprintf("%d %s are here.", len(freshIdxs), coloredPlural) - } - var suffix string - if multi && (len(timed) > 0 || len(depleted) > 0) { - suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) - } - qualityTimer := "" - if showTimers && len(instances) > 0 && instances[0].quality > 0 { - qualityTimer = fmt.Sprintf(" (Burning for %d more ticks)", instances[0].quality) - } - sess.WriteLine(fmt.Sprintf("%s%s%s", line, suffix, qualityTimer)) + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) } - - for _, ins := range timed { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) - } - sess.WriteLine(fmt.Sprintf("%s%s%s", line, tag, timer)) + timer := "" + if showTimers { + timer = fmt.Sprintf(" (despawn: %d/%d)", ins.sharedCur, ins.sharedMax) } + lines = append(lines, fmt.Sprintf("%s%s%s", line, tag, timer)) + } - for _, ins := range depleted { - var line string - if roomDesc != "" { - line = roomDesc - } else { - line = fmt.Sprintf("A %s is here.", coloredName) - } - tag := "" - if multi { - tag = fmt.Sprintf(" [%d]", ins.idx) - } - timer := "" - if showTimers { - timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) - } - sess.WriteLine(fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) + for _, ins := range depleted { + var line string + if roomDesc != "" { + line = roomDesc + } else { + line = fmt.Sprintf("A %s is here.", coloredName) + } + tag := "" + if multi { + tag = fmt.Sprintf(" [%d]", ins.idx) + } + timer := "" + if showTimers { + timer = fmt.Sprintf(" (respawns in %d ticks)", ins.respawnIn) } + lines = append(lines, fmt.Sprintf("%s (depleted)%s%s", line, tag, timer)) } } - g.showFarmPatches(sess, p) + lines = append(lines, g.showFarmPatches(sess, p)...) + return lines } -func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) { +func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string { ground := g.World.GroundItemsDetailed(p.RoomID) - if len(ground) > 0 { - showDespawn := p.OptionBool("despawn") - showReserve := p.OptionBool("reserve") + if len(ground) == 0 { + return nil + } + showDespawn := p.OptionBool("despawn") + showReserve := p.OptionBool("reserve") - type displayLine struct { - name string - quantity int - colorName string - annotation string - } + type displayLine struct { + name string + quantity int + colorName string + annotation string + } - type groupKey struct { - itemID string - despawnTimer int - } + type groupKey struct { + itemID string + despawnTimer int + } - var lines []displayLine - groups := make(map[groupKey]*displayLine) - var groupOrder []groupKey + var lines []displayLine + groups := make(map[groupKey]*displayLine) + var groupOrder []groupKey - for _, info := range ground { - def, err := g.ItemStore.Load(info.ItemID) - name := info.ItemID - if err == nil { - name = def.Name - } - coloredName := g.itemColorize(sess, def, name) + for _, info := range ground { + def, err := g.ItemStore.Load(info.ItemID) + name := info.ItemID + if err == nil { + name = def.Name + } + coloredName := g.itemColorize(sess, def, name) - if info.ReservedFor != "" { - var parts []string - if showReserve { - parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) - } else { - parts = append(parts, "reserved") - } - if showDespawn && !info.IsSpawn { - parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) - } - annotation := "" - if len(parts) > 0 { - annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) - } - lines = append(lines, displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - }) - continue + if info.ReservedFor != "" { + var parts []string + if showReserve { + parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) + } else { + parts = append(parts, "reserved") } + if showDespawn && !info.IsSpawn { + parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) + } + annotation := "" + if len(parts) > 0 { + annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) + } + lines = append(lines, displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + }) + continue + } - timerKey := -1 + timerKey := -1 + if showDespawn && !info.IsSpawn { + timerKey = info.DespawnTimer + } + key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} + + if existing, ok := groups[key]; ok { + existing.quantity += info.Quantity + } else { + annotation := "" if showDespawn && !info.IsSpawn { - timerKey = info.DespawnTimer + annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) } - key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} - - if existing, ok := groups[key]; ok { - existing.quantity += info.Quantity - } else { - annotation := "" - if showDespawn && !info.IsSpawn { - annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) - } - dl := &displayLine{ - name: name, - quantity: info.Quantity, - colorName: coloredName, - annotation: annotation, - } - groups[key] = dl - groupOrder = append(groupOrder, key) + dl := &displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, } + groups[key] = dl + groupOrder = append(groupOrder, key) } + } - for _, key := range groupOrder { - lines = append(lines, *groups[key]) - } + for _, key := range groupOrder { + lines = append(lines, *groups[key]) + } - sort.Slice(lines, func(i, j int) bool { - return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) - }) + sort.Slice(lines, func(i, j int) bool { + return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) + }) - sess.WriteLine("") - sess.WriteLine("On the ground:") + var out []string + out = append(out, "") + out = append(out, "On the ground:") - type fmtLine struct { - prefix string - annotation string - } - var fmtLines []fmtLine - maxPrefix := 0 + type fmtLine struct { + prefix string + annotation string + } + var fmtLines []fmtLine + maxPrefix := 0 - for _, dl := range lines { - prefix := "" - if dl.quantity > 1 { - prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) - } else { - prefix = fmt.Sprintf(" %s", dl.colorName) - } - if visibleLen(prefix) > maxPrefix { - maxPrefix = visibleLen(prefix) - } - fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) + for _, dl := range lines { + prefix := "" + if dl.quantity > 1 { + prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) + } else { + prefix = fmt.Sprintf(" %s", dl.colorName) } + if visibleLen(prefix) > maxPrefix { + maxPrefix = visibleLen(prefix) + } + fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) + } - for _, l := range fmtLines { - if l.annotation != "" { - pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) - sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) - } else { - sess.WriteLine(l.prefix) - } + for _, l := range fmtLines { + if l.annotation != "" { + pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) + out = append(out, fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) + } else { + out = append(out, l.prefix) } } + return out } func (g *Game) showRoomExits(sess *net.Session, p *player.Player, room *world.Room) { @@ -763,8 +779,8 @@ func wrapText(text string, width int) []string { return lines } -func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLines []string, mapLines []string) { - total := len(descLines) +func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, contentLines []string, mapLines []string) { + total := len(contentLines) if len(mapLines) > total { total = len(mapLines) } @@ -774,22 +790,23 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine mapWidth = 70 } for i := 0; i < total; i++ { - desc := "" - if i < len(descLines) { - desc = descLines[i] + content := "" + if i < len(contentLines) { + content = contentLines[i] } - mapLine := "" if i < len(mapLines) { - mapLine = mapLines[i] - } - if leftMap { - sess.WriteLine(fmt.Sprintf("%s %s", mapLine, desc)) - } else { - pad := mapWidth + (len(desc) - visibleLen(desc)) - if pad < 0 { - pad = 0 + mapLine := mapLines[i] + if leftMap { + sess.WriteLine(fmt.Sprintf("%s %s", mapLine, content)) + } else { + pad := mapWidth + (len(content) - visibleLen(content)) + if pad < 0 { + pad = 0 + } + sess.WriteLine(fmt.Sprintf("%-*s %s", pad, content, mapLine)) } - sess.WriteLine(fmt.Sprintf("%-*s %s", pad, desc, mapLine)) + } else { + sess.WriteLine(content) } } } -- cgit v1.2.3