package game import ( "fmt" "sort" "strconv" "strings" "thehouseoficarus/internal/color" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/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 (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID))), ) descWidth := p.OptionInt("room_desc_width") if descWidth <= 0 { descWidth = 70 } rawLines := wrapText(room.Description, descWidth) mode := g.colorMode(sess) roomDescSpec := g.resolveColor(sess, "room_desc") descLines := make([]string, len(rawLines)) for i, l := range rawLines { descLines[i] = color.ExpandTagsWithDefault(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) } } 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]", g.colorize(sess, "enemy_hp", 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 } npcColor := "hostile_npc" if m.Protected { npcColor = "friendly_npc" } 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, npcColor, displayName), levelStr, 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 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)) } 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)) } 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)) } } } ground := g.World.GroundItemsDetailed(p.RoomID) if len(ground) > 0 { showDespawn := p.OptionBool("despawn") showReserve := p.OptionBool("reserve") type displayLine struct { name string quantity int colorName string annotation string } type groupKey struct { itemID string despawnTimer int } 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) 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 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 { 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) } } 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) }) sess.WriteLine("") sess.WriteLine("On the ground:") 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 _, 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) } } } if len(room.Exits) > 0 { sess.WriteLine("") if p.OptionBool("exits") { sess.WriteLine("Exits:") type exitLine struct { dir string targetName string } var lines []exitLine maxDirLen := 0 for _, dir := range world.ExitOrder { exitDef, ok := room.Exits[dir] if !ok { continue } coloredDir := g.colorize(sess, "exit_direction", string(dir)) targetRoom, err := g.World.LoadRoom(exitDef.Room) targetName := g.colorize(sess, "room_number", fmt.Sprintf("#%d", exitDef.Room)) if err == nil { targetName = g.colorize(sess, "exit_name", targetRoom.Name) } if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { targetName += " (blocked)" } lines = append(lines, exitLine{coloredDir, targetName}) if visibleLen(coloredDir) > maxDirLen { maxDirLen = visibleLen(coloredDir) } } for _, l := range lines { pad := maxDirLen + (len(l.dir) - visibleLen(l.dir)) sess.WriteLine(fmt.Sprintf(" %-*s - %s", pad, l.dir, l.targetName)) } } else { sess.Write("Exits: ") first := true for _, dir := range world.ExitOrder { if _, ok := room.Exits[dir]; ok { if !first { sess.Write(", ") } sess.Write(g.colorize(sess, "exit_direction", 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", g.colorize(sess, "player_name", 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 { npcColor := "hostile_npc" if best.Protected { npcColor = "friendly_npc" } mobLevel := mobCombatLevel(best) levelStr := g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel)) sess.WriteLines( "", fmt.Sprintf("%s %s", g.colorize(sess, npcColor, best.Name), levelStr), ) 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 def.Description != "" && !strings.Contains(def.Description, "{quality}") { sess.WriteLine(fmt.Sprintf(" %s", def.Description)) } 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 && strings.Contains(def.Description, "{quality}") { desc := strings.ReplaceAll(def.Description, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) sess.WriteLine(fmt.Sprintf(" %s", desc)) } } 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( "", g.itemColorize(sess, def, 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{ "", g.itemColorize(sess, def, 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) { myP := sess.Player.(*player.Player) theirLevel := p.CombatLevel() levelStr := g.levelColorize(sess, myP.CombatLevel(), theirLevel, fmt.Sprint(theirLevel)) sess.WriteLines( "", p.Name, fmt.Sprintf(" Combat Level: %s", levelStr), 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 var itemDef *object.ItemDef if def, err := g.ItemStore.Load(itemID); err == nil { name = def.Name itemDef = def } sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, itemDef, 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 } type exitLine struct { dir string name string } var lines []exitLine maxDirLen := 0 for _, dir := range world.ExitOrder { exitDef, ok := room.Exits[dir] if !ok { continue } coloredDir := g.colorize(sess, "exit_direction", string(dir)) targetRoom, err := g.World.LoadRoom(exitDef.Room) targetName := g.colorize(sess, "room_number", fmt.Sprintf("#%d", exitDef.Room)) if err == nil { targetName = g.colorize(sess, "exit_name", targetRoom.Name) } lines = append(lines, exitLine{coloredDir, targetName}) if visibleLen(coloredDir) > maxDirLen { maxDirLen = visibleLen(coloredDir) } } for _, l := range lines { pad := maxDirLen + (len(l.dir) - visibleLen(l.dir)) sess.WriteLine(fmt.Sprintf(" %-*s - %s", pad, l.dir, l.name)) } } 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 visibleLen(s string) int { return color.VisibleLen(s) } 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 visibleLen(current)+1+visibleLen(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 { pad := mapWidth + (len(desc) - visibleLen(desc)) if pad < 0 { pad = 0 } sess.WriteLine(fmt.Sprintf("%-*s %s", pad, desc, mapLine)) } } }