package game import ( "fmt" "sort" "strconv" "strings" "thirdcollapse/internal/combat" "thirdcollapse/internal/net" "thirdcollapse/internal/player" "thirdcollapse/internal/world" ) func (g *Game) doLook(sess *net.Session) { p := sess.Player.(*player.Player) room, err := g.World.LoadRoom(p.RoomID) if err != nil { sess.WriteLine("You are in a void.") return } sess.WriteLines( "", fmt.Sprintf("%s (#%d)", room.Name, room.ID), ) descWidth := p.OptionInt("room_desc_width") if descWidth <= 0 { descWidth = 70 } descLines := wrapText(room.Description, descWidth) 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) } } 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("") for _, m := range mobs { hp := "" if m.HP < m.MaxHP { hp = fmt.Sprintf(" [%d/%dhp]", 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 } sess.WriteLine(fmt.Sprintf(" %s (level %d)%s%s", displayName, mobCombatLevel(m), hp, desc)) } } 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]++ } for _, objID := range order { count := grouped[objID] def, err := g.ObjectStore.Load(objID) if err != nil || def.Hidden { continue } 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) } } roomDesc := def.InRoomDescription if len(freshIdxs) > 0 { var line string if roomDesc != "" { line = roomDesc } else if len(freshIdxs) == 1 { line = "A " + def.Name + " is here." } else { line = fmt.Sprintf("%d %ss are here.", len(freshIdxs), def.Name) } 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)) } for _, ins := range timed { var line string if roomDesc != "" { line = roomDesc } else { line = "A " + def.Name + " is here." } 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)) } for _, ins := range depleted { var line string if roomDesc != "" { line = roomDesc } else { line = "A " + def.Name + " is here." } 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)) } } } ground := g.World.GroundItemsDetailed(p.RoomID) if len(ground) > 0 { sort.Slice(ground, func(i, j int) bool { ni := ground[i].ItemID if def, err := g.ItemStore.Load(ground[i].ItemID); err == nil { ni = def.Name } nj := ground[j].ItemID if def, err := g.ItemStore.Load(ground[j].ItemID); err == nil { nj = def.Name } return strings.ToLower(ni) < strings.ToLower(nj) }) sess.WriteLine("") sess.WriteLine("On the ground:") type groundLine struct { prefix string reserved string } var lines []groundLine maxPrefix := 0 for _, info := range ground { def, err := g.ItemStore.Load(info.ItemID) name := info.ItemID if err == nil { name = def.Name } prefix := "" if info.Quantity > 1 { prefix = fmt.Sprintf(" %d x %s", info.Quantity, name) } else { prefix = fmt.Sprintf(" %s", name) } if len(prefix) > maxPrefix { maxPrefix = len(prefix) } var parts []string if info.ReservedFor != "" { if p.OptionBool("reserve") { parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) } else { parts = append(parts, "reserved") } } if p.OptionBool("despawn") && !info.IsSpawn { parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) } reserved := "" if len(parts) > 0 { reserved = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) } lines = append(lines, groundLine{prefix, reserved}) } for _, l := range lines { if l.reserved != "" { sess.WriteLine(fmt.Sprintf("%-*s%s", maxPrefix+1, l.prefix, l.reserved)) } else { sess.WriteLine(l.prefix) } } } if len(room.Exits) > 0 { sess.WriteLine("") if p.OptionBool("exits") { sess.WriteLine("Exits:") for _, dir := range world.ExitOrder { exitDef, ok := room.Exits[dir] if !ok { continue } targetRoom, err := g.World.LoadRoom(exitDef.Room) targetName := fmt.Sprintf("#%d", exitDef.Room) if err == nil { targetName = targetRoom.Name } if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { targetName += " (blocked)" } sess.WriteLine(fmt.Sprintf(" %-6s - %s", dir, targetName)) } } else { sess.Write("Exits: ") first := true for _, dir := range world.ExitOrder { if _, ok := room.Exits[dir]; ok { if !first { sess.Write(", ") } sess.Write(string(dir)) first = false } } sess.WriteLine("") } } others := g.Hub.PlayersInRoom(p.RoomID) for _, other := range others { if other != sess && other.Player != nil { op := other.Player.(*player.Player) line := fmt.Sprintf("\n%s is here", op.Name) if cs := combat.GetCombat(op.Name); cs != nil { if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 { name := mobDisplayName(mob, false) if idx := mobInstanceIdx(mob, g.MobStore.MobsInRoom(p.RoomID)); idx > 0 { name += fmt.Sprintf(" [%d]", idx) } line += fmt.Sprintf(" (fighting %s)", name) } } else if as, ok := op.ActionState.(*ActionState); ok && as != nil { if desc := as.Description(); desc != "" { line += ", " + desc } } sess.WriteLine(line + ".") } } } func (g *Game) doLookTarget(sess *net.Session, input string) { p := sess.Player.(*player.Player) lower := strings.ToLower(input) if exitDir := g.World.ResolveExit(lower); exitDir != "" { room, err := g.World.LoadRoom(p.RoomID) if err != nil { sess.WriteLine("You can't see anything that way.") return } exitDef, ok := room.Exits[exitDir] if !ok { sess.WriteLine("You can't see anything that way.") return } g.World.SeedGroundItems(exitDef.Room) g.seedRoomMobs(exitDef.Room) origRoom := p.RoomID p.RoomID = exitDef.Room g.doLook(sess) p.RoomID = origRoom return } var best *world.MobInstance bestQ := world.MatchNone for _, m := range g.MobStore.MobsInRoom(p.RoomID) { q := m.MatchQuality(lower) if q > bestQ { bestQ = q best = m } } if best != nil { sess.WriteLines( "", fmt.Sprintf("%s (level %d)", best.Name, mobCombatLevel(best)), ) if best.IdleDescription != "" { sess.WriteLine(fmt.Sprintf(" %s", best.IdleDescription)) } sess.WriteLines( "", fmt.Sprintf(" Attack: %d", best.Attack), fmt.Sprintf(" Strength: %d", best.Strength), fmt.Sprintf(" Defense: %d", best.Defense), fmt.Sprintf(" HP: %d/%d", best.HP, best.MaxHP), ) return } instances := g.World.FindObjInstances(p.RoomID, lower) if len(instances) > 0 { st := &instances[0] def, _ := g.ObjectStore.Load(st.DefID) sess.WriteLine("") if len(instances) > 1 { sess.WriteLine(fmt.Sprintf("%d %ss:", len(instances), def.Name)) } else { sess.WriteLine(def.Name) } if desc, ok := def.Props["description"].(string); ok && desc != "" { sess.WriteLine(fmt.Sprintf(" %s", desc)) } if p.OptionBool("depletion") { for _, ist := range instances { if ist.Depleted { if len(instances) > 1 { sess.WriteLine(fmt.Sprintf(" %s %d: depleted, respawns in %d ticks.", def.Name, ist.Index+1, int(ist.DepleteTimer))) } else { sess.WriteLine(fmt.Sprintf(" Depleted, respawns in %d ticks.", int(ist.DepleteTimer))) } } else if ist.SharedMax > 0 && float64(ist.SharedTimer) < ist.SharedMax { if len(instances) > 1 { sess.WriteLine(fmt.Sprintf(" %s %d: despawn timer %d/%.0f.", def.Name, ist.Index+1, ist.SharedTimer, ist.SharedMax)) } else { sess.WriteLine(fmt.Sprintf(" Despawn timer: %d/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) } } } } for _, ist := range instances { if ist.Quality > 0 { if qdesc, ok := def.Props["quality_description"].(string); ok && qdesc != "" { qdesc = strings.ReplaceAll(qdesc, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) sess.WriteLine(fmt.Sprintf(" %s", qdesc)) } } } return } ground := g.World.GroundItems(p.RoomID) for itemID := range ground { def, err := g.ItemStore.Load(itemID) if err != nil || !def.MatchesName(input) { continue } sess.WriteLines( "", def.Name, fmt.Sprintf(" %s", def.Description), fmt.Sprintf(" Value: %d credits", def.Value), ) return } for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil || !def.MatchesName(input) { continue } lines := []string{ "", def.Name, fmt.Sprintf(" %s", def.Description), fmt.Sprintf(" Value: %d credits", def.Value), } if slot.MaxQuality > 0 { lines = append(lines, fmt.Sprintf(" Has %d units of butane left.", slot.Quality)) } sess.WriteLines(lines...) return } others := g.Hub.PlayersInRoom(p.RoomID) for _, other := range others { if other == sess || other.Player == nil { continue } op := other.Player.(*player.Player) if strings.ToLower(op.Name) != lower { continue } showPlayerInfo(g, sess, op) return } sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) } func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) { sess.WriteLines( "", p.Name, fmt.Sprintf(" Combat Level: %d", p.CombatLevel()), fmt.Sprintf(" HP: %d/%d", p.HP, p.MaxHP()), "", ) for _, s := range player.AllSkills { level := p.Level(s) sess.WriteLine(fmt.Sprintf(" %-12s Level: %d", s, level)) } sess.WriteLine("") sess.WriteLine(" Equipment:") for _, slot := range EquipSlots { itemID, ok := p.Equipment[slot] if !ok { continue } name := itemID if def, err := g.ItemStore.Load(itemID); err == nil { name = def.Name } sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, name)) } if p.Description != "" { sess.WriteLine("") sess.WriteLine(fmt.Sprintf(" %s", p.Description)) } } func (g *Game) doExits(sess *net.Session) { p := sess.Player.(*player.Player) room, err := g.World.LoadRoom(p.RoomID) if err != nil || len(room.Exits) == 0 { sess.WriteLine("There are no exits here.") return } for _, dir := range world.ExitOrder { exitDef, ok := room.Exits[dir] if !ok { continue } targetRoom, err := g.World.LoadRoom(exitDef.Room) targetName := fmt.Sprintf("#%d", exitDef.Room) if err == nil { targetName = targetRoom.Name } sess.WriteLine(fmt.Sprintf(" %-6s - %s", dir, targetName)) } } func joinInts(nums []int) string { var parts []string for _, n := range nums { parts = append(parts, strconv.Itoa(n)) } return strings.Join(parts, ",") } func mobInstanceIdx(mob *world.MobInstance, roomMobs []*world.MobInstance) int { var same []*world.MobInstance for _, m := range roomMobs { if m.DefID == mob.DefID { same = append(same, m) } } if len(same) <= 1 { return 0 } sort.Slice(same, func(i, j int) bool { return same[i].InstanceID < same[j].InstanceID }) for i, m := range same { if m.InstanceID == mob.InstanceID { return i + 1 } } 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.OptionString("tiny_map") == "left" mapWidth := p.OptionInt("room_desc_width") if mapWidth <= 0 { mapWidth = 70 } 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", mapWidth, desc, mapLine)) } } }