go_games_collection/corpsestarch_tui.go

285 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"math/rand"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
)
// corpseStarchTickMsg — сигнал раз в секунду для начисления
// пассивного дохода в реальном времени, пока открыт экран игры.
type corpseStarchTickMsg struct{ At time.Time }
const corpseStarchTickInterval = 1 * time.Second
// corpseStarchAutosaveMsg — сигнал периодического автосохранения.
type corpseStarchAutosaveMsg struct{}
const corpseStarchAutosaveInterval = 15 * time.Second
// CorpseStarchModel — TUI поверх CorpseStarchGameState.
type CorpseStarchModel struct {
game *CorpseStarchGameState
message string
quitting bool
backToMenu bool
}
// NewCorpseStarchModel загружает сохранённую партию, если она есть
// (начисляя честный пассивный доход за время отсутствия), либо
// создаёт новую.
func NewCorpseStarchModel() CorpseStarchModel {
now := time.Now()
g, err := LoadCorpseStarchGame()
if err != nil {
return CorpseStarchModel{game: NewCorpseStarchGame(now)}
}
before := g.Starch
g.Tick(now) // честно начисляем доход за время, пока партия была закрыта
m := CorpseStarchModel{game: g}
if earned := g.Starch - before; earned >= 1 {
m.message = Tf("corpsestarch.msg.welcome_back", formatStarch(earned))
}
return m
}
func (m CorpseStarchModel) Init() tea.Cmd {
return tea.Batch(corpseStarchScheduleTick(), corpseStarchScheduleAutosave())
}
func corpseStarchScheduleTick() tea.Cmd {
return tea.Tick(corpseStarchTickInterval, func(t time.Time) tea.Msg { return corpseStarchTickMsg{At: t} })
}
func corpseStarchScheduleAutosave() tea.Cmd {
return tea.Tick(corpseStarchAutosaveInterval, func(time.Time) tea.Msg { return corpseStarchAutosaveMsg{} })
}
func (m *CorpseStarchModel) setInfo(key string) {
m.message = T(key)
}
// corpseStarchAction — один пункт динамического пронумерованного
// меню действий: что показать и что сделать по нажатию цифры.
type corpseStarchAction struct {
Label string
Handle func(m *CorpseStarchModel, now time.Time)
}
// buildActions собирает список доступных прямо сейчас действий —
// от этого зависит, какая цифра что делает; список пересобирается
// на каждый рендер/обработку ввода, поэтому нумерация всегда
// соответствует актуально показанным пунктам.
func (m CorpseStarchModel) buildActions(now time.Time) []corpseStarchAction {
g := m.game
var actions []corpseStarchAction
actions = append(actions, corpseStarchAction{
Label: Tf("corpsestarch.action.requisition", formatStarch(g.currentClickYield(now))),
Handle: func(m *CorpseStarchModel, now time.Time) {
m.game.Requisition(now)
m.setInfo("corpsestarch.msg.requisitioned")
},
})
if g.ServitorsUnlocked() {
for i, def := range corpseStarchServitorDefs {
if !g.ServitorUnlocked(i) {
continue
}
i := i
cost := g.servitorCost(i)
actions = append(actions, corpseStarchAction{
Label: Tf("corpsestarch.action.buy_servitor", T(def.NameKey), g.ServitorCounts[i], formatStarch(cost)),
Handle: func(m *CorpseStarchModel, now time.Time) {
if m.game.BuyServitor(i) {
m.setInfo("corpsestarch.msg.servitor_bought")
} else {
m.setInfo("corpsestarch.msg.cannot_afford")
}
},
})
}
}
if g.ShopUnlocked() {
cost := g.clickUpgradeCost()
actions = append(actions, corpseStarchAction{
Label: Tf("corpsestarch.action.upgrade_click", formatStarch(cost)),
Handle: func(m *CorpseStarchModel, now time.Time) {
if m.game.BuyClickUpgrade() {
m.setInfo("corpsestarch.msg.upgrade_bought")
} else {
m.setInfo("corpsestarch.msg.cannot_afford")
}
},
})
}
if g.SumpUnlocked() {
actions = append(actions, corpseStarchAction{
Label: T("corpsestarch.action.dip"),
Handle: func(m *CorpseStarchModel, now time.Time) {
outcome := m.game.DipChainsword(now, rand.Float64())
switch outcome {
case DipBonusStarch:
m.setInfo("corpsestarch.log.dip_bonus")
case DipMachineSpiritBlessing:
m.setInfo("corpsestarch.log.dip_blessing")
case DipNothing:
m.setInfo("corpsestarch.log.dip_nothing")
case DipCorruption:
m.setInfo("corpsestarch.log.dip_corruption")
}
},
})
}
if g.MissionsUnlocked() {
if g.ActiveMission == nil {
for i, def := range corpseStarchMissionDefs {
if !g.MissionUnlocked(i) {
continue
}
i := i
actions = append(actions, corpseStarchAction{
Label: Tf("corpsestarch.action.start_mission", T(def.NameKey), formatDuration(def.Duration)),
Handle: func(m *CorpseStarchModel, now time.Time) {
if m.game.StartMission(now, i) {
m.setInfo("corpsestarch.msg.mission_started")
}
},
})
}
} else if g.MissionRemaining(now) == 0 {
actions = append(actions, corpseStarchAction{
Label: T("corpsestarch.action.collect_mission"),
Handle: func(m *CorpseStarchModel, now time.Time) {
if _, ok := m.game.CollectMission(now, rand.Float64()); ok {
m.setInfo("corpsestarch.log.mission_done")
}
},
})
}
}
return actions
}
func (m CorpseStarchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKey(msg)
case corpseStarchTickMsg:
m.game.Tick(msg.At)
return m, corpseStarchScheduleTick()
case corpseStarchAutosaveMsg:
_ = SaveCorpseStarchGame(m.game)
return m, corpseStarchScheduleAutosave()
}
return m, nil
}
func (m CorpseStarchModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
if key == "ctrl+c" {
_ = SaveCorpseStarchGame(m.game)
m.quitting = true
return m, tea.Quit
}
if key == "q" {
_ = SaveCorpseStarchGame(m.game)
m.backToMenu = true
return m, nil
}
now := time.Now()
actions := m.buildActions(now)
for i := 0; i < len(actions) && i < 9; i++ {
if key == fmt.Sprintf("%d", i+1) {
actions[i].Handle(&m, now)
return m, nil
}
}
return m, nil
}
// --- Отрисовка -----------------------------------------------------
// formatStarch форматирует значение ресурса компактно (без длинного
// хвоста дробной части).
func formatStarch(v float64) string {
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%.1f", v)
}
func formatDuration(d time.Duration) string {
d = d.Round(time.Second)
m := int(d.Minutes())
s := int(d.Seconds()) % 60
if m > 0 {
return fmt.Sprintf("%dm%02ds", m, s)
}
return fmt.Sprintf("%ds", s)
}
func (m CorpseStarchModel) View() string {
if m.quitting {
return T("common.goodbye")
}
g := m.game
now := time.Now()
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("corpsestarch.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, Tf("corpsestarch.header.starch", formatStarch(g.Starch), formatStarch(g.TotalEarned)))
if g.SumpUnlocked() {
fmt.Fprintln(&b, Tf("corpsestarch.header.corruption", g.Corruption))
}
fmt.Fprintln(&b)
if g.MissionsUnlocked() && g.ActiveMission != nil {
def := corpseStarchMissionDefs[g.ActiveMission.DefIdx]
remaining := g.MissionRemaining(now)
if remaining > 0 {
fmt.Fprintln(&b, Tf("corpsestarch.mission.inprogress", T(def.NameKey), formatDuration(remaining)))
} else {
fmt.Fprintln(&b, winStyle.Render(Tf("corpsestarch.mission.ready", T(def.NameKey))))
}
fmt.Fprintln(&b)
}
actions := m.buildActions(now)
for i, a := range actions {
if i >= 9 {
break
}
fmt.Fprintf(&b, "[%d] %s\n", i+1, a.Label)
}
fmt.Fprintln(&b)
if len(g.Log) > 0 {
fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.log.title")))
for _, key := range g.Log {
fmt.Fprintln(&b, dimStyle.Render(" "+T(key)))
}
fmt.Fprintln(&b)
}
if m.message != "" {
fmt.Fprintln(&b, infoStyle.Render(m.message))
fmt.Fprintln(&b)
}
fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.hint")))
return b.String()
}