skyrama_tui/internal/ui/airport_view.go
2026-06-04 20:51:20 +03:00

125 lines
3.7 KiB
Go

package ui
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"skyrama-tui/internal/model"
"skyrama-tui/internal/style"
)
// AirportView renders the full airport overview panel.
// Uses a single panel with two sections to avoid nested-panel line-count overhead.
func AirportView(airport *model.Airport, country string, width int) string {
if airport == nil {
return lipgloss.JoinVertical(lipgloss.Left,
style.PanelHeader.Render("🛫 Airport"),
"",
style.Muted.Render("No airport found for your account."),
style.Muted.Render("Ask an admin to create one, or use the db_manager tool."),
)
}
barW := 22
fuelBar := style.ResourceBar(
int(airport.CurrentFuel), atLeast1(int(airport.MaxFuelCapacity)), barW, style.ColorFuel)
fuelLine := row(
style.FuelChip.Width(14).Render("⛽ Fuel"),
fuelBar,
style.Muted.Render(fmt.Sprintf(" %d / %d", airport.CurrentFuel, airport.MaxFuelCapacity)),
style.Muted.Render(fmt.Sprintf(" +%d/min", airport.FuelGenerationRate)),
)
paxBar := style.ResourceBar(
int(airport.CurrentPassengers), atLeast1(int(airport.MaxPassengerCapacity)), barW, style.ColorPassenger)
paxLine := row(
style.PassengerChip.Width(14).Render("👤 Passengers"),
paxBar,
style.Muted.Render(fmt.Sprintf(" %d / %d", airport.CurrentPassengers, airport.MaxPassengerCapacity)),
style.Muted.Render(fmt.Sprintf(" +%d/min", airport.PassengerGenerationRate)),
)
cashLine := style.CashChip.Render(fmt.Sprintf("💰 Cash $%s", formatCash(airport.Cash)))
xpNeeded := model.XPToNextLevel(airport.PlayerLevel)
xpCurrent := model.XPIntoLevel(airport.PlayerLevel, airport.ExperiencePoints)
xpBar := style.ResourceBar(xpCurrent, atLeast1(xpNeeded), barW, style.ColorGreen)
xpLine := row(
style.Bold.Width(14).Render("✨ XP"),
xpBar,
style.Muted.Render(fmt.Sprintf(" %d / %d", xpCurrent, xpNeeded)),
)
avail := style.StatusIdle.Render("Open ✓")
if !airport.IsAvailable {
avail = style.StatusMaint.Render("Closed ✗")
}
sinceSec := time.Since(airport.ResourcesLastUpdatedAt)
syncStr := "just now"
if sinceSec >= time.Second {
syncStr = sinceSec.Round(time.Second).String() + " ago"
}
// Single panel containing both sections — avoids double-border line overhead.
panel := style.Panel.Width(width).Render(lipgloss.JoinVertical(lipgloss.Left,
style.PanelHeader.Render("Resources"),
fuelLine,
paxLine,
cashLine,
xpLine,
"",
style.PanelHeader.Render("Airport Info"),
kv("Name", airport.Name),
kv("Country", country),
kv("Level", fmt.Sprintf("Lv.%d", airport.PlayerLevel)),
kv("Status", avail),
kv("Last sync", syncStr),
))
// 1-line header to keep total view height minimal.
header := lipgloss.JoinHorizontal(lipgloss.Top,
style.Bold.Foreground(style.ColorSky).Width(width/2).Render("🛫 "+airport.Name),
style.Muted.Width(width/2).Align(lipgloss.Right).
Render(fmt.Sprintf("Lv.%d 📍 %s", airport.PlayerLevel, country)),
)
return lipgloss.JoinVertical(lipgloss.Left, header, panel)
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
func row(parts ...string) string {
return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
}
func kv(key, val string) string {
return lipgloss.JoinHorizontal(lipgloss.Top,
style.Muted.Width(14).Render(key+":"),
val,
)
}
func formatCash(v uint64) string {
s := fmt.Sprintf("%d", v)
var b strings.Builder
for i, ch := range s {
if i > 0 && (len(s)-i)%3 == 0 {
b.WriteRune(',')
}
b.WriteRune(ch)
}
return b.String()
}
// atLeast1 prevents division-by-zero in progress bar calculations.
func atLeast1(n int) int {
if n <= 0 {
return 1
}
return n
}