clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/app-model-decomp
puzzletea / ui / stats_view.go
stats_view.go 5.2K
1package ui23import (4 "fmt"5 "image/color"6 "strings"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/stats"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213func statsCardFrameStyle() lipgloss.Style {14 return lipgloss.NewStyle().15 Border(lipgloss.RoundedBorder()).16 BorderForeground(theme.Current().Border).17 Padding(0, 1).18 Width(stats.CardInnerWidth + 4)19}2021func statsCardTitleStyle(fg color.Color) lipgloss.Style {22 return lipgloss.NewStyle().Bold(true).Foreground(fg)23}2425func statsLabelStyle() lipgloss.Style {26 return lipgloss.NewStyle().Foreground(theme.Current().TextDim)27}2829func statsValueStyle() lipgloss.Style {30 return lipgloss.NewStyle().Foreground(theme.Current().Accent).Bold(true)31}3233func statsXPBarFilledStyle() lipgloss.Style {34 return lipgloss.NewStyle().Foreground(theme.Current().Accent)35}3637func statsXPBarEmptyStyle() lipgloss.Style {38 return lipgloss.NewStyle().Foreground(theme.Current().Border)39}4041func statsXPFractionStyle() lipgloss.Style {42 return lipgloss.NewStyle().Foreground(theme.Current().FG)43}4445func statsBannerRuleStyle() lipgloss.Style {46 return lipgloss.NewStyle().Foreground(theme.Current().Border)47}4849func statsBannerLabelStyle() lipgloss.Style {50 return lipgloss.NewStyle().Foreground(theme.Current().FG)51}5253func statsBannerValueStyle() lipgloss.Style {54 return lipgloss.NewStyle().Foreground(theme.Current().Accent).Bold(true)55}5657func RenderStatsCard(c stats.Card, titleColor color.Color) string {58 title := statsCardTitleStyle(titleColor).Render(59 fmt.Sprintf("%s \u2014 LVL %d", strings.ToUpper(c.GameType), c.Level),60 )6162 lines := []string{63 title,64 renderStatsStatLine("Preferred Mode:", c.PreferredMode),65 renderStatsStatLine("Total Victories:", fmt.Sprintf("%d", c.Victories)),66 renderStatsStatLine("Total Attempts:", fmt.Sprintf("%d", c.Attempts)),67 renderStatsStatLine("Daily Played:", fmt.Sprintf("%d", c.DailyPlayed)),68 "",69 renderStatsXPBar(c.CurrentXP, c.Level),70 }7172 return statsCardFrameStyle().Render(strings.Join(lines, "\n"))73}7475func renderStatsStatLine(label, value string) string {76 l := statsLabelStyle().Render(label)77 v := statsValueStyle().Render(value)78 gap := stats.CardInnerWidth - lipgloss.Width(l) - lipgloss.Width(v)79 gap = max(gap, 1)80 return l + strings.Repeat(" ", gap) + v81}8283func renderStatsXPBar(currentXP, level int) string {84 currentLevelXP := stats.XPForLevel(level)85 nextLevelXP := stats.XPForLevel(level + 1)86 xpIntoLevel := currentXP - currentLevelXP87 xpNeeded := nextLevelXP - currentLevelXP88 if xpNeeded <= 0 {89 xpNeeded = 190 }9192 const barWidth = 1293 filled := 094 if xpNeeded > 0 {95 filled = int(float64(xpIntoLevel) / float64(xpNeeded) * barWidth)96 }97 filled = max(0, min(filled, barWidth))9899 bar := statsXPBarFilledStyle().Render(strings.Repeat("\u2588", filled)) +100 statsXPBarEmptyStyle().Render(strings.Repeat("\u2591", barWidth-filled))101 frac := statsXPFractionStyle().Render(fmt.Sprintf(" %d/%d", xpIntoLevel, xpNeeded))102 return statsLabelStyle().Foreground(theme.Current().FG).Render("XP ") + bar + frac103}104105func RenderStatsBanner(b stats.ProfileBanner, width int) string {106 if width < 20 {107 width = 40108 }109110 ruleStr := statsBannerRuleStyle().Render(111 "\u2500\u2500\u2500 Profile " + strings.Repeat("\u2500", max(width-12, 1)),112 )113114 streakStr := fmt.Sprintf("%d days", b.DailyStreak)115 if b.DailyStreak == 1 {116 streakStr = "1 day"117 }118 streakSuffix := "( )"119 if b.CurrentDaily {120 streakSuffix = "(\u2714)"121 }122 streakStr += " " + streakSuffix123124 col1 := statsBannerLabelStyle().Render("Profile Level: ") + statsBannerValueStyle().Render(fmt.Sprintf("%d", b.ProfileLevel))125 col2 := statsBannerLabelStyle().Render("Daily Streak: ") + statsBannerValueStyle().Render(streakStr)126 col3 := statsBannerLabelStyle().Render("Total Dailies: ") + statsBannerValueStyle().Render(fmt.Sprintf("%d", b.TotalDailies))127 col4 := statsBannerLabelStyle().Render("Weeklies Completed: ") +128 statsBannerValueStyle().Render(fmt.Sprintf("%d total (%2d/99 this week)", b.WeekliesCompleted, b.ThisWeekHighestIndex))129130 gap := width - lipgloss.Width(col1) - lipgloss.Width(col2)131 gap = max(gap, 2)132 row1 := col1 + strings.Repeat(" ", gap) + col2133134 gap2 := width - lipgloss.Width(col3) - lipgloss.Width(col4)135 gap2 = max(gap2, 2)136 row2 := col3 + strings.Repeat(" ", gap2) + col4137138 bottomRule := statsBannerRuleStyle().Render(strings.Repeat("\u2500", max(width, 1)))139 return strings.Join([]string{ruleStr, row1, row2, bottomRule}, "\n")140}141142func RenderStatsCardGrid(cards []stats.Card, width int) string {143 if len(cards) == 0 {144 return lipgloss.NewStyle().145 Foreground(theme.Current().TextDim).146 Render("No stats yet \u2014 play some puzzles!")147 }148149 cols := 1150 if width > stats.CardFullWidth {151 cols = max((width+2)/(stats.CardFullWidth+2), 1)152 }153 palette := theme.Current().ThemeColors()154 rendered := make([]string, len(cards))155 for i, c := range cards {156 rendered[i] = RenderStatsCard(c, palette[i%len(palette)])157 }158159 rows := make([]string, 0, (len(rendered)+cols-1)/cols)160 for i := 0; i < len(rendered); i += cols {161 end := min(i+cols, len(rendered))162 row := rendered[i]163 for j := i + 1; j < end; j++ {164 row = lipgloss.JoinHorizontal(lipgloss.Top, row, strings.Repeat(" ", 2), rendered[j])165 }166 rows = append(rows, row)167 }168 return strings.Join(rows, strings.Repeat("\n", 3))169}