203 lines
5.6 KiB
Go
203 lines
5.6 KiB
Go
|
|
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
|
||
|
|
}
|