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 := g.colorizeConstant(sess, "score_max_value", 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, "currency_pickup", formatInt(p.Credits)) chipsStr := g.colorize(sess, "currency_pickup", formatInt(countChips(p))) styleStr := color.Render(mode, g.resolveConstant(sess, "table_header_label"), 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 := g.hpBarStyle(sess, p.HP, p.MaxHP()) hpBarStr := ui.RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode) hpFg := g.hpBarStyle(sess, p.HP, p.MaxHP()).FilledColor hpLine := fmt.Sprintf("HP [%s] %s / %s", hpBarStr, color.Render(mode, color.ColorSpec{Fg: hpFg}, fmt.Sprint(p.HP)), g.colorizeConstant(sess, "score_max_value", fmt.Sprint(p.MaxHP()))) content(hpLine) batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode) batBarStr := color.Render(mode, g.resolveConstant(sess, "battery_bar"), batBarRaw) batFg := g.resolveConstant(sess, "battery_bar").Fg batLine := fmt.Sprintf("BAT [%s] %s / %s", batBarStr, color.Render(mode, color.ColorSpec{Fg: batFg}, fmt.Sprintf("%.1f", p.Battery)), g.colorizeConstant(sess, "score_max_value", fmt.Sprintf("%.0f", p.MaxBattery()))) content(batLine) engSpec := g.resolveConstant(sess, "run_energy_bar") engEmpty := g.resolveConstant(sess, "hp_bar_empty") engStyle := &ui.BarStyle{FilledColor: engSpec.Fg, EmptyColor: engEmpty.Fg, EmptyDim: engEmpty.Dim} engBarStr := ui.RenderColoredBar(p.RunEnergy, p.MaxRunEnergy(), 26, unicode, engStyle, mode) engLine := fmt.Sprintf("ENG [%s] %s / %s", engBarStr, color.Render(mode, color.ColorSpec{Fg: engSpec.Fg}, fmt.Sprint(p.RunEnergy)), g.colorizeConstant(sess, "score_max_value", fmt.Sprint(p.MaxRunEnergy()))) content(engLine) 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 := g.skillLevelColor(sess, level) name := padded(color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), 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, g.resolveConstant(sess, "active_tech_on"), "[ON]") name := color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), 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, g.resolveConstant(sess, "active_tech_status"), "[BUFF]"), buff.BonusPercent, color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), 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, ",") } // skillLevelColor returns the xterm256 palette index for a skill level cell // on the datapad score panel, sourcing it from the constant_colors section. func (g *Game) skillLevelColor(sess *net.Session, level int) int { spec := g.resolveConstant(sess, "skill_lvl_base") switch { case level >= 99: spec = g.resolveConstant(sess, "skill_lvl_max") case level >= 80: spec = g.resolveConstant(sess, "skill_lvl_high") case level >= 60: spec = g.resolveConstant(sess, "skill_lvl_mid") case level >= 40: spec = g.resolveConstant(sess, "skill_lvl_low") } return spec.Fg } 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) }