package game import ( "fmt" "strconv" "strings" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/ui" ) func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) { g.doScore(sess) } func (g *Game) doScore(sess *net.Session) { p := sess.Player mode := g.colorMode(sess) unicode := p.OptionBool("unicode") h, v, tl, tr, ml, mr, bl, br := boxGlyphs(unicode) const boxW = 72 const innerW = boxW - 4 var out []string border := func(l, r string) { out = append(out, l+strings.Repeat(h, boxW-2)+r) } content := func(text string) { out = append(out, v+" "+padLine(text, innerW)+" "+v) } sep := func() { border(ml, mr) } gap := func() { content("") } padded := func(text string, width int) string { pad := width - color.VisibleLen(text) if pad < 0 { pad = 0 } return text + strings.Repeat(" ", pad) } skillDisplay := map[player.SkillName]string{ player.Accuracy: "Accuracy", player.Strength: "Strength", player.Defense: "Defense", player.Hitpoints: "Hitpoints", player.Ranged: "Ranged", player.Science: "Science", player.Technology: "Technology", player.Assassin: "Assassin", player.Fishing: "Fishing", player.Woodcutting: "Woodcutting", player.Mining: "Mining", player.Hacking: "Hacking", player.Scavenging: "Scavenging", player.Farming: "Farming", player.Thieving: "Thieving", player.Cooking: "Cooking", player.Smithing: "Smithing", player.Crafting: "Crafting", player.Fletching: "Fletching", player.Pharmacy: "Pharmacy", player.Construction: "Construction", player.Firemaking: "Firemaking", player.Agility: "Agility", } type skillCat struct { Name string Skills []player.SkillName } categories := []skillCat{ {"Combat", []player.SkillName{ player.Accuracy, player.Strength, player.Defense, player.Hitpoints, player.Ranged, player.Science, player.Technology, player.Assassin, }}, {"Gathering", []player.SkillName{ player.Fishing, player.Woodcutting, player.Mining, player.Hacking, player.Scavenging, player.Thieving, }}, {"Production", []player.SkillName{ player.Cooking, player.Smithing, player.Crafting, player.Fletching, player.Pharmacy, player.Construction, player.Farming, }}, {"Utility", []player.SkillName{ player.Agility, player.Firemaking, }}, } // ── Top border + title ── border(tl, tr) titleSep := strings.Repeat(h, 3) title := titleSep + " D A T A P A D " + titleSep titleVL := color.VisibleLen(title) leftPad := (innerW - titleVL) / 2 rightPad := innerW - titleVL - leftPad content(strings.Repeat(" ", leftPad) + title + strings.Repeat(" ", rightPad)) // ── Vitals ── sep() nameStr := g.colorize(sess, "player_name", p.Name) clStr := color.Render(mode, color.Parse("FA"), fmt.Sprintf("Combat Lvl %d", p.CombatLevel())) roomStr := g.colorize(sess, "room_number", fmt.Sprintf("Room #%d", p.RoomID)) row1 := padded(nameStr, 20) + padded(clStr, 24) + roomStr content(row1) creditsStr := g.colorize(sess, "credits_pickup", formatInt(p.Credits)) chipsStr := g.colorize(sess, "chips_pickup", formatInt(countChips(p))) styleStr := color.Render(mode, color.Parse("49"), fmt.Sprintf("Style: %s", p.AttackStyle)) invStr := fmt.Sprintf("Inv: %d/28", 28-p.FreeSlots()) row2 := padded("Credits: "+creditsStr, 19) + padded("Chips: "+chipsStr, 16) + padded(styleStr, 22) + invStr content(row2) gap() hpStyle := hpBarStyle(p.HP, p.MaxHP()) hpBarStr := ui.RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode) hpFg := hpBarFg(p.HP, p.MaxHP()) hpLine := fmt.Sprintf("HP [%s] %s / %s", hpBarStr, color.Render(mode, color.ColorSpec{Fg: hpFg}, fmt.Sprint(p.HP)), color.Render(mode, color.Parse("FA"), fmt.Sprint(p.MaxHP()))) content(hpLine) batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode) batBarStr := color.Render(mode, color.ColorSpec{Fg: 110}, batBarRaw) batLine := fmt.Sprintf("BAT [%s] %s / %s", batBarStr, color.Render(mode, color.ColorSpec{Fg: 110}, fmt.Sprintf("%.1f", p.Battery)), color.Render(mode, color.Parse("FA"), fmt.Sprintf("%.0f", p.MaxBattery()))) content(batLine) gap() actionDesc := "Idle" if d := g.playerActionDisplay(p); d != "" { actionDesc = d } content("Action: " + actionDesc) // ── Skills ── totalLevel := 0 for _, s := range player.AllSkills { totalLevel += p.Level(s) } sep() content(fmt.Sprintf("Skills %s Total Level %d", strings.Repeat(h, 2), totalLevel)) gap() bullet := "\u25c6" if !unicode { bullet = "*" } buildSkillCell := func(s player.SkillName) string { level := p.Level(s) lvlColor := skillLevelColor(level) name := padded(color.Render(mode, color.ColorSpec{Fg: 110}, skillDisplay[s]), 12) lvl := color.Render(mode, color.ColorSpec{Fg: lvlColor}, fmt.Sprintf("%2d", level)) xpBar := xpProgressBar(p.Skills[s], 12, unicode) return fmt.Sprintf("%s %s [%s]", name, lvl, xpBar) } for _, cat := range categories { content(" " + bullet + " " + cat.Name) skills := cat.Skills for i := 0; i < len(skills); i += 2 { left := buildSkillCell(skills[i]) row := left if i+1 < len(skills) { right := buildSkillCell(skills[i+1]) row += " " + right } content(" " + row) } gap() } // ── Statistics ── sep() stats := p.Stats statsLine := fmt.Sprintf("Rooms Explored: %d Kills: %d Deaths: %d", stats.RoomsVisited.Len(), stats.TotalKills, stats.Deaths) content(statsLine) content(fmt.Sprintf("APS Nodes Found: %d", len(getKnownApsNodes(p)))) // ── Active Systems ── if len(p.ActiveTechs) > 0 || len(p.ActiveBuffs) > 0 { sep() } if len(p.ActiveTechs) > 0 { var techParts []string for _, id := range p.ActiveTechList() { def := GetTechDef(id) if def != nil { on := color.Render(mode, color.ColorSpec{Fg: 108}, "[ON]") name := color.Render(mode, color.ColorSpec{Fg: 110}, def.Name) techParts = append(techParts, on+" "+name) } } content(strings.Join(techParts, " ")) } if len(p.ActiveBuffs) > 0 { for _, buff := range p.ActiveBuffs { buffLine := fmt.Sprintf("%s +%d%% %s (%d ticks remaining)", color.Render(mode, color.ColorSpec{Fg: 108}, "[BUFF]"), buff.BonusPercent, color.Render(mode, color.ColorSpec{Fg: 110}, buff.Stat), buff.TicksLeft) content(buffLine) } } // ── Bottom border ── border(bl, br) for _, line := range out { sess.WriteLine(line) } } func boxGlyphs(unicode bool) (h, v, tl, tr, ml, mr, bl, br string) { if unicode { return "═", "║", "╔", "╗", "╠", "╣", "╚", "╝" } return "-", "|", "+", "+", "+", "+", "+", "+" } func padLine(text string, width int) string { pad := width - color.VisibleLen(text) if pad < 0 { pad = 0 } return text + strings.Repeat(" ", pad) } func formatInt(n int) string { if n < 1000 { return strconv.Itoa(n) } s := strconv.Itoa(n) var parts []string for i := len(s); i > 0; i -= 3 { start := i - 3 if start < 0 { start = 0 } parts = append([]string{s[start:i]}, parts...) } return strings.Join(parts, ",") } func hpBarStyle(current, max int) *ui.BarStyle { if max <= 0 { max = 1 } pct := float64(current) / float64(max) var fg int switch { case pct >= 0.7: fg = 108 case pct >= 0.4: fg = 179 default: fg = 167 } return &ui.BarStyle{FilledColor: fg, EmptyColor: 237, EmptyDim: true} } func hpBarFg(current, max int) int { if max <= 0 { max = 1 } pct := float64(current) / float64(max) switch { case pct >= 0.7: return 108 case pct >= 0.4: return 179 default: return 167 } } func skillLevelColor(level int) int { switch { case level >= 99: return 180 case level >= 80: return 179 case level >= 60: return 116 case level >= 40: return 73 default: return 66 } } func xpProgressBar(xp int, width int, unicode bool) string { currentLevel := player.LevelForXP(xp) if currentLevel >= 99 { fillRune, _ := ui.BarRunes(unicode) return strings.Repeat(fillRune, width) } xpForLevel := player.XPForLevel(currentLevel) xpForNext := player.XPForLevel(currentLevel + 1) progress := xp - xpForLevel needed := xpForNext - xpForLevel if needed <= 0 { needed = 1 } fillRune, emptyRune := ui.BarRunes(unicode) filled := progress * width / needed if filled > width { filled = width } if filled < 0 { filled = 0 } return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled) }