aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_score.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-21 03:46:25 -0400
committerhistoria <[not public]>2026-06-21 03:46:25 -0400
commit84072ffe6365ad57c4976e9272762aa6db41de7e (patch)
treed911389fd91e129871c23ec539e67a2666d9f78a /internal/game/cmd_score.go
parente4bc2de6e6aa507044a6a75b4c1954974539a857 (diff)
downloadthehouseoficarus-84072ffe6365ad57c4976e9272762aa6db41de7e.tar.gz
feat: better score output
Diffstat (limited to 'internal/game/cmd_score.go')
-rw-r--r--internal/game/cmd_score.go323
1 files changed, 281 insertions, 42 deletions
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 523dcb9..d0302c2 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -3,6 +3,7 @@ package game
import (
"fmt"
"strconv"
+ "strings"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
@@ -16,62 +17,300 @@ func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) {
func (g *Game) doScore(sess *net.Session) {
p := sess.Player
mode := g.colorMode(sess)
- sess.WriteLines(
- "",
- fmt.Sprintf("Name: %s", g.colorize(sess, "player_name", p.Name)),
- fmt.Sprintf("Combat Level: %s", color.Render(mode, color.Parse("230"), fmt.Sprint(p.CombatLevel()))),
- fmt.Sprintf("HP: %s/%s", color.Render(mode, color.Parse("84"), fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))),
- fmt.Sprintf("Battery: %s/%s", g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)), color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery()))),
- fmt.Sprintf("Credits: %s", g.colorize(sess, "credits_pickup", fmt.Sprint(p.Credits))),
- )
-
- t := &Table{Title: "Skills", Columns: []string{
- color.Render(mode, color.Parse("75"), "Skill"),
- color.Render(mode, color.Parse("245"), "Abbr"),
- color.Render(mode, color.Parse("230"), "Level"),
- color.Render(mode, color.Parse("222"), "XP"),
- color.Render(mode, color.Parse("179"), "XP to Next"),
- }}
+ 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.Attack: "Attack", 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.Attack, 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("230"), 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))
+ styleStr := color.Render(mode, color.Parse("75"), fmt.Sprintf("Style: %s", p.AttackStyle))
+ invStr := fmt.Sprintf("Inv: %d/28 used", 28-p.FreeSlots())
+ row2 := padded("Credits: "+creditsStr, 20) + padded(styleStr, 24) + invStr
+ content(row2)
+
+ gap()
+
+ hpStyle := hpBarStyle(p.HP, p.MaxHP())
+ hpBarStr := 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("230"), fmt.Sprint(p.MaxHP())))
+ content(hpLine)
+
+ batBarRaw := RenderBarF(p.Battery, p.MaxBattery(), 26, unicode)
+ batBarStr := color.Render(mode, color.ColorSpec{Fg: 45}, batBarRaw)
+ batLine := fmt.Sprintf("BAT [%s] %s / %s",
+ batBarStr,
+ color.Render(mode, color.ColorSpec{Fg: 45}, fmt.Sprintf("%.1f", p.Battery)),
+ color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery())))
+ content(batLine)
+
+ gap()
+
+ actionDesc := "Idle"
+ if as, ok := p.ActionState.(*ActionState); ok && as != nil {
+ if d := as.Description(); 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)
- xp := p.Skills[s]
- next := player.XPForNextLevel(xp)
- t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse("75"), string(s)),
- color.Render(mode, color.Parse("245"), player.SkillAbbr[s]),
- color.Render(mode, color.Parse("230"), strconv.Itoa(level)),
- color.Render(mode, color.Parse("222"), strconv.Itoa(xp)),
- color.Render(mode, color.Parse("179"), strconv.Itoa(next)),
- })
- }
- for _, line := range t.Render(p.OptionBool("unicode")) {
- sess.WriteLine(line)
+ lvlColor := skillLevelColor(level)
+ name := padded(color.Render(mode, color.ColorSpec{Fg: 75}, 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",
+ len(stats.RoomsVisited), 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 {
- sess.WriteLine("")
- sess.WriteLine("Active Tech:")
+ var techParts []string
for _, id := range p.ActiveTechList() {
def := GetTechDef(id)
if def != nil {
- sess.WriteLine(fmt.Sprintf(" %s %s",
- color.Render(mode, color.Parse("82"), "[ON]"),
- color.Render(mode, color.Parse("75"), def.Name),
- ))
+ on := color.Render(mode, color.ColorSpec{Fg: 82}, "[ON]")
+ name := color.Render(mode, color.ColorSpec{Fg: 75}, def.Name)
+ techParts = append(techParts, on+" "+name)
}
}
+ content(strings.Join(techParts, " "))
}
-
if len(p.ActiveBuffs) > 0 {
- sess.WriteLine("")
- sess.WriteLine("Active Buffs:")
for _, buff := range p.ActiveBuffs {
- sess.WriteLine(fmt.Sprintf(" %s +%d%% %s (%d ticks)",
- color.Render(mode, color.Parse("82"), "[BUFF]"),
+ buffLine := fmt.Sprintf("%s +%d%% %s (%d ticks remaining)",
+ color.Render(mode, color.ColorSpec{Fg: 82}, "[BUFF]"),
buff.BonusPercent,
- color.Render(mode, color.Parse("75"), buff.Stat),
- buff.TicksLeft,
- ))
+ color.Render(mode, color.ColorSpec{Fg: 75}, 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) *BarStyle {
+ if max <= 0 {
+ max = 1
+ }
+ pct := float64(current) / float64(max)
+ var fg int
+ switch {
+ case pct >= 0.7:
+ fg = 84
+ case pct >= 0.4:
+ fg = 208
+ default:
+ fg = 196
+ }
+ return &BarStyle{FilledColor: fg, EmptyColor: 238, EmptyDim: true}
+}
+
+func hpBarFg(current, max int) int {
+ if max <= 0 {
+ max = 1
+ }
+ pct := float64(current) / float64(max)
+ switch {
+ case pct >= 0.7:
+ return 84
+ case pct >= 0.4:
+ return 208
+ default:
+ return 196
+ }
+}
+
+func skillLevelColor(level int) int {
+ switch {
+ case level >= 99:
+ return 196
+ case level >= 80:
+ return 214
+ case level >= 60:
+ return 208
+ case level >= 40:
+ return 220
+ default:
+ return 40
+ }
+}
+
+func xpProgressBar(xp int, width int, unicode bool) string {
+ currentLevel := player.LevelForXP(xp)
+ if currentLevel >= 99 {
+ fillRune, _ := 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 := 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)
}