phase 0.3
This commit is contained in:
commit
4a94bfc4e7
52 changed files with 11653 additions and 0 deletions
203
internal/ui/shop.go
Normal file
203
internal/ui/shop.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue