diff options
| author | historia <[not public]> | 2026-06-21 03:46:25 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-21 03:46:25 -0400 |
| commit | 84072ffe6365ad57c4976e9272762aa6db41de7e (patch) | |
| tree | d911389fd91e129871c23ec539e67a2666d9f78a /internal/game/cmd_score.go | |
| parent | e4bc2de6e6aa507044a6a75b4c1954974539a857 (diff) | |
| download | thehouseoficarus-84072ffe6365ad57c4976e9272762aa6db41de7e.tar.gz | |
feat: better score output
Diffstat (limited to 'internal/game/cmd_score.go')
| -rw-r--r-- | internal/game/cmd_score.go | 323 |
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) } |
