package ui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "skyrama-tui/internal/model" "skyrama-tui/internal/service" "skyrama-tui/internal/style" ) // ─── Messages ───────────────────────────────────────────────────────────────── type ShopLoadedMsg struct{ Planes []model.Plane } type ShopErrMsg struct{ Err error } type PlaneBoughtMsg struct{ Name string } // ─── List item ──────────────────────────────────────────────────────────────── type shopPlaneItem struct{ p model.Plane } func (s shopPlaneItem) Title() string { return fmt.Sprintf("%s %s %s", planeIcon(s.p.AircraftType), s.p.Name, style.CashChip.Render(fmt.Sprintf("$%d", s.p.BuyingCostCash)), ) } func (s shopPlaneItem) Description() string { parts := []string{ style.Muted.Render(fmt.Sprintf("lv.%d", s.p.MinLevel)), style.Muted.Render(s.p.AircraftSize + "/" + s.p.AircraftType), style.FuelChip.Render(fmt.Sprintf("⛽%d", s.p.OperationCostFuel)), style.Muted.Render(fmt.Sprintf("⏱%s", fmtDuration(int(s.p.TravelTimeInSeconds)))), style.Muted.Render(fmt.Sprintf("✨%d xp", s.p.CompletedFlightExperience)), } if s.p.MaxPassengers.Valid && s.p.MaxPassengers.Int32 > 0 { parts = append(parts, style.PassengerChip.Render(fmt.Sprintf("👤%d", s.p.MaxPassengers.Int32))) } if s.p.CompletedFlightCashReward.Valid { parts = append(parts, style.CashChip.Render(fmt.Sprintf("💰%d/flight", s.p.CompletedFlightCashReward.Int32))) } return strings.Join(parts, " ") } func (s shopPlaneItem) FilterValue() string { return s.p.Name } // ─── ShopModel ──────────────────────────────────────────────────────────────── type ShopModel struct { client *service.GameService airportID uint32 playerLevel uint32 list list.Model planes []model.Plane statusMsg string focused bool width int height int } func NewShopModel(client *service.GameService, airportID uint32, playerLevel uint32) ShopModel { d := list.NewDefaultDelegate() d.Styles.SelectedTitle = d.Styles.SelectedTitle. Foreground(style.ColorGold). BorderForeground(style.ColorGold) d.Styles.SelectedDesc = d.Styles.SelectedDesc. Foreground(style.ColorPassenger). BorderForeground(style.ColorGold) l := list.New(nil, d, 0, 0) l.Title = "" l.SetShowTitle(false) l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(true) return ShopModel{ client: client, airportID: airportID, playerLevel: playerLevel, list: l, } } func (m ShopModel) Init() tea.Cmd { return m.load() } func (m ShopModel) Update(msg tea.Msg) (ShopModel, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height m.list.SetSize(m.width-4, m.height-12) case ShopLoadedMsg: m.planes = msg.Planes m.list.SetItems(m.planesToItems()) case PlaneBoughtMsg: m.statusMsg = style.Success.String() + "Bought: " + msg.Name + "!" return m, m.load() case ShopErrMsg: m.statusMsg = style.Error.String() + msg.Err.Error() case tea.KeyMsg: if !m.focused { break } switch msg.String() { case "r": m.statusMsg = "" return m, m.load() case "enter", " ", "b": item, ok := m.list.SelectedItem().(shopPlaneItem) if !ok { break } if item.p.MinLevel > m.playerLevel { m.statusMsg = style.Warning.String() + fmt.Sprintf("Requires level %d (you are level %d)", item.p.MinLevel, m.playerLevel) break } return m, m.buy(item.p) } } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } func (m ShopModel) View() string { header := style.PanelHeader.Render("🛒 Plane Shop") levelNote := style.Info.String() + style.Muted.Render( fmt.Sprintf("Your level: %d", m.playerLevel), ) statusLine := "" if m.statusMsg != "" { statusLine = "\n" + m.statusMsg + "\n" } help := style.HelpKey.Render("enter / b") + style.HelpDesc.Render(" buy ") + style.HelpKey.Render("r") + style.HelpDesc.Render(" refresh ") + style.HelpKey.Render("/") + style.HelpDesc.Render(" filter") return lipgloss.JoinVertical(lipgloss.Left, header, levelNote, statusLine, m.list.View(), "", style.Muted.Render(help), ) } // ─── Exported for parent AppModel ───────────────────────────────────────────── // Load is the exported version for use by parent AppModel. func (m ShopModel) Load() tea.Cmd { return m.load() } func (m ShopModel) load() tea.Cmd { client := m.client return func() tea.Msg { planes, err := client.GetAllPlanes() if err != nil { return ShopErrMsg{Err: err} } return ShopLoadedMsg{Planes: planes} } } func (m ShopModel) buy(p model.Plane) tea.Cmd { client := m.client airportID := m.airportID planeID := p.ID planeName := p.Name return func() tea.Msg { // The service finds a free hangar and parks the plane in one intent. if err := client.BuyPlane(airportID, planeID); err != nil { return ShopErrMsg{Err: err} } return PlaneBoughtMsg{Name: planeName} } } func (m ShopModel) planesToItems() []list.Item { items := make([]list.Item, len(m.planes)) for i, p := range m.planes { items[i] = shopPlaneItem{p: p} } return items }