go_games_collection/wayfarer_tui.go
2026-07-07 09:12:09 +03:00

977 lines
28 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"
"sort"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// wayfarerView — текущий экран игры.
type wayfarerView int
const (
wfViewHub wayfarerView = iota
wfViewMarket
wfViewShipyard
wfViewTravel
wfViewCombat
wfViewQuestList
wfViewQuestStep
wfViewQuestOffer
wfViewGameOver
)
// wayfarerRefuelPrice / wayfarerRepairPrice — цены за единицу
// топлива/прочности корпуса на верфи (фиксированные — свои для
// каждой станции цены усложнили бы UI без особой пользы).
const (
wayfarerRefuelPrice = 4.0
wayfarerRepairPrice = 6.0
)
// wayfarerEquipmentCost — стоимость разового приобретения снаряжения.
var wayfarerEquipmentCost = map[WayfarerEquipment]float64{
EquipFuelScoop: 800,
EquipCargoExpansion: 1200,
EquipShieldBooster: 1500,
EquipEscapePod: 2000,
EquipBetterLaser: 1800,
}
var wayfarerEquipmentNameKey = map[WayfarerEquipment]string{
EquipFuelScoop: "wf.equip.fuelscoop",
EquipCargoExpansion: "wf.equip.cargoexp",
EquipShieldBooster: "wf.equip.shieldbooster",
EquipEscapePod: "wf.equip.escapepod",
EquipBetterLaser: "wf.equip.betterlaser",
}
var wayfarerEquipmentOrder = []WayfarerEquipment{
EquipFuelScoop, EquipCargoExpansion, EquipShieldBooster, EquipEscapePod, EquipBetterLaser,
}
// wayfarerStartSystem — фиксированная стартовая система новой
// карьеры (детерминированно — как "Lave" в классической Elite).
var wayfarerStartSystem = WayfarerSystemID{Galaxy: 0, Index: 0}
// WayfarerModel — TUI поверх WayfarerShip.
type WayfarerModel struct {
ship *WayfarerShip
view wayfarerView
combat *WayfarerCombatState
combatRewardCredits float64 // награда за успешный бой/сигнал бедствия, если применимо
cursor int // курсор для рынка/верфи/перелёта/квестов (что уместно в текущем view)
travelCandidates []WayfarerSystemID
pendingQuestKey string // ключ квеста, предложенного встречей в пути, но ещё не принятого/отклонённого
marketPrices []float64 // цены на товары в текущей системе — считаются один раз при стыковке, не на каждый рендер (см. rollMarketPrices)
confirmingReset bool // ждём подтверждения "начать новую карьеру" (y/n) — необратимое действие
message string
isError bool
rng *rand.Rand
termWidth int // для переноса длинного текста квестов/сообщений по ширине реального терминала
quitting bool
backToMenu bool
}
// NewWayfarerModel загружает сохранённую карьеру, если она есть, или
// начинает новую с фиксированной стартовой системы.
func NewWayfarerModel() WayfarerModel {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
var m WayfarerModel
if ship, err := LoadWayfarerShip(); err == nil {
m = WayfarerModel{ship: ship, view: wfViewHub, rng: rng, termWidth: 80}
} else {
m = WayfarerModel{ship: NewWayfarerShip(wayfarerStartSystem), view: wfViewHub, rng: rng, termWidth: 80}
}
m.rollMarketPrices()
return m
}
func (m WayfarerModel) Init() tea.Cmd { return nil }
func (m *WayfarerModel) setInfo(key string) {
m.message = T(key)
m.isError = false
}
func (m *WayfarerModel) setInfof(key string, args ...interface{}) {
m.message = Tf(key, args...)
m.isError = false
}
func (m *WayfarerModel) setError(err error) {
m.message = T(err.Error())
m.isError = true
}
func (m WayfarerModel) currentSystem() WayfarerSystem {
return GenerateSystem(m.ship.CurrentSystem)
}
// rollMarketPrices пересчитывает цены на товары в текущей системе.
// Вызывается один раз при стыковке (старт игры, загрузка карьеры,
// успешный прыжок) — а не при каждом рендере рынка, иначе цены
// мелькали бы на каждое нажатие клавиши, даже просто при движении
// курсора по списку товаров.
func (m *WayfarerModel) rollMarketPrices() {
sys := m.currentSystem()
m.marketPrices = make([]float64, len(WayfarerCommodities))
for i := range WayfarerCommodities {
m.marketPrices[i] = CommodityPrice(i, sys, m.rng)
}
}
func (m WayfarerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
m.termWidth = sizeMsg.Width
return m, nil
}
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return m, nil
}
return m.handleKey(keyMsg)
}
func (m WayfarerModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
if key == "ctrl+c" {
_ = SaveWayfarerShip(m.ship)
m.quitting = true
return m, tea.Quit
}
if m.confirmingReset {
switch key {
case "y", "Y":
_ = DeleteWayfarerSave()
m.ship = NewWayfarerShip(wayfarerStartSystem)
m.confirmingReset = false
m.combat = nil
m.pendingQuestKey = ""
m.view = wfViewHub
m.rollMarketPrices()
m.setInfo("wf.msg.reset_done")
case "n", "N", "esc":
m.confirmingReset = false
m.message = ""
}
return m, nil
}
switch m.view {
case wfViewHub:
return m.handleHubKey(key)
case wfViewMarket:
return m.handleMarketKey(key)
case wfViewShipyard:
return m.handleShipyardKey(key)
case wfViewTravel:
return m.handleTravelKey(key)
case wfViewCombat:
return m.handleCombatKey(key)
case wfViewQuestList:
return m.handleQuestListKey(key)
case wfViewQuestStep:
return m.handleQuestStepKey(key)
case wfViewQuestOffer:
return m.handleQuestOfferKey(key)
case wfViewGameOver:
return m.handleGameOverKey(key)
}
return m, nil
}
// --- Хаб станции -----------------------------------------------------
func (m WayfarerModel) handleHubKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "q":
_ = SaveWayfarerShip(m.ship)
m.backToMenu = true
return m, nil
case "1":
m.view = wfViewMarket
m.cursor = 0
m.message = ""
case "2":
m.view = wfViewShipyard
m.cursor = 0
m.message = ""
case "3":
m.travelCandidates = wayfarerNearbySystems(m.ship.CurrentSystem, m.ship.FuelRange())
m.view = wfViewTravel
m.cursor = 0
m.message = ""
case "4":
m.view = wfViewQuestList
m.cursor = 0
m.message = ""
case "N":
m.confirmingReset = true
}
return m, nil
}
// --- Рынок -------------------------------------------------------------
func (m WayfarerModel) handleMarketKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "q", "esc":
m.view = wfViewHub
m.message = ""
return m, nil
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(WayfarerCommodities)-1 {
m.cursor++
}
case "b", "s", "B", "S":
price := m.marketPrices[m.cursor]
switch key {
case "b":
if err := m.ship.BuyCommodity(m.cursor, 1, price); err != nil {
m.setError(err)
} else {
m.setInfof("wf.msg.bought", 1, T(WayfarerCommodities[m.cursor].Key))
}
case "B":
qty := m.ship.CargoFree()
affordable := int(m.ship.Credits / price)
if affordable < qty {
qty = affordable
}
if err := m.ship.BuyCommodity(m.cursor, qty, price); err != nil {
m.setError(err)
} else {
m.setInfof("wf.msg.bought", qty, T(WayfarerCommodities[m.cursor].Key))
}
case "s":
sellPrice := SellPrice(price)
if err := m.ship.SellCommodity(m.cursor, 1, sellPrice); err != nil {
m.setError(err)
} else {
m.setInfof("wf.msg.sold", 1, T(WayfarerCommodities[m.cursor].Key))
}
case "S":
sellPrice := SellPrice(price)
qty := m.ship.Cargo[m.cursor]
if err := m.ship.SellCommodity(m.cursor, qty, sellPrice); err != nil {
m.setError(err)
} else {
m.setInfof("wf.msg.sold", qty, T(WayfarerCommodities[m.cursor].Key))
}
}
}
return m, nil
}
// --- Верфь ---------------------------------------------------------------
func (m WayfarerModel) shipyardActionCount() int {
return 2 + len(wayfarerEquipmentOrder) // дозаправка + ремонт + снаряжение
}
func (m WayfarerModel) handleShipyardKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "q", "esc":
m.view = wfViewHub
m.message = ""
return m, nil
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < m.shipyardActionCount()-1 {
m.cursor++
}
case "enter", " ":
switch {
case m.cursor == 0:
need := m.ship.FuelCapacity - m.ship.Fuel
if err := m.ship.Refuel(need, wayfarerRefuelPrice); err != nil {
m.setError(err)
} else {
m.setInfo("wf.msg.refueled")
}
case m.cursor == 1:
need := m.ship.MaxHull - m.ship.Hull
if err := m.ship.Repair(need, wayfarerRepairPrice); err != nil {
m.setError(err)
} else {
m.setInfo("wf.msg.repaired")
}
default:
eq := wayfarerEquipmentOrder[m.cursor-2]
if m.ship.Equipment[eq] {
m.setError(errWayfarerAlreadyOwned)
return m, nil
}
cost := wayfarerEquipmentCost[eq]
if m.ship.Credits < cost {
m.setError(errWayfarerInsufficientFunds)
return m, nil
}
m.ship.Credits -= cost
m.ship.Equipment[eq] = true
applyWayfarerEquipmentEffect(m.ship, eq)
m.setInfo("wf.msg.equipped")
}
}
return m, nil
}
// applyWayfarerEquipmentEffect применяет однократный эффект покупки
// снаряжения к статам корабля (там, где это применимо — щитовой
// бустер и улучшенный лазер сразу повышают соответствующие статы).
func applyWayfarerEquipmentEffect(s *WayfarerShip, eq WayfarerEquipment) {
switch eq {
case EquipShieldBooster:
s.MaxShield += 40
s.Shield += 40
case EquipBetterLaser:
s.LaserPower += 8
case EquipCargoExpansion:
s.CargoCapacity += 15
case EquipFuelScoop:
s.FuelCapacity += 3
s.Fuel += 3
}
}
var errWayfarerAlreadyOwned = wayfarerErr("это снаряжение уже установлено")
// --- Перелёт ---------------------------------------------------------------
// wayfarerNearbySystems находит до 9 ближайших систем в пределах
// maxRange световых лет от current (в той же галактике), плюс —
// отдельным пунктом в конце списка — возможность гиперпрыжка в
// следующую галактику (см. render/handle travel).
func wayfarerNearbySystems(current WayfarerSystemID, maxRange float64) []WayfarerSystemID {
from := GenerateSystem(current)
type candidate struct {
id WayfarerSystemID
dist float64
}
var candidates []candidate
for i := 0; i < WayfarerSystemsPerGalaxy; i++ {
if i == current.Index {
continue
}
id := WayfarerSystemID{Galaxy: current.Galaxy, Index: i}
dist := from.Distance(GenerateSystem(id))
if dist <= maxRange {
candidates = append(candidates, candidate{id, dist})
}
}
sort.Slice(candidates, func(i, j int) bool { return candidates[i].dist < candidates[j].dist })
if len(candidates) > 9 {
candidates = candidates[:9]
}
out := make([]WayfarerSystemID, len(candidates))
for i, c := range candidates {
out[i] = c.id
}
return out
}
func (m WayfarerModel) handleTravelKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "q", "esc":
m.view = wfViewHub
m.message = ""
return m, nil
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
max := len(m.travelCandidates) // +1 пункт межгалактического прыжка обрабатывается отдельным индексом
if m.cursor < max {
m.cursor++
}
case "enter", " ":
if m.cursor == len(m.travelCandidates) {
return m.performGalacticJump()
}
if m.cursor >= len(m.travelCandidates) {
return m, nil
}
target := m.travelCandidates[m.cursor]
return m.performJump(target)
}
return m, nil
}
// performGalacticJump — межгалактический прыжок в случайную систему
// следующей галактики (по кругу после последней). Требует почти
// полного бака — платит собой весь текущий запас топлива.
func (m WayfarerModel) performGalacticJump() (tea.Model, tea.Cmd) {
if m.ship.Fuel < m.ship.FuelCapacity*0.9 {
m.setError(errWayfarerNeedFullTank)
return m, nil
}
nextGalaxy := (m.ship.CurrentSystem.Galaxy + 1) % WayfarerGalaxyCount
target := WayfarerSystemID{Galaxy: nextGalaxy, Index: m.rng.Intn(WayfarerSystemsPerGalaxy)}
m.ship.Fuel = 0
m.ship.CurrentSystem = target
m.ship.Visited[target] = true
m.rollMarketPrices()
m.setInfo("wf.msg.galactic_jump")
m.view = wfViewHub
return m, nil
}
var errWayfarerNeedFullTank = wayfarerErr("для межгалактического прыжка нужен полный (или почти полный) бак")
// performJump выполняет обычный прыжок и разрешает случайную
// встречу в пути, если она произошла.
func (m WayfarerModel) performJump(target WayfarerSystemID) (tea.Model, tea.Cmd) {
if err := m.ship.JumpTo(target); err != nil {
m.setError(err)
return m, nil
}
m.rollMarketPrices()
dest := GenerateSystem(target)
encounter := RollEncounter(dest, m.rng)
switch encounter {
case EncounterNone:
if questKey, ok := RollQuestOffer(m.ship, m.rng); ok {
m.pendingQuestKey = questKey
m.view = wfViewQuestOffer
m.message = ""
return m, nil
}
m.setInfo("wf.msg.jump_quiet")
m.view = wfViewHub
case EncounterPirate:
hull, laser := GeneratePirateStats(dest, m.rng)
m.combat = NewWayfarerCombat("wf.enemy.pirate", hull, laser,
m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true)
m.combatRewardCredits = float64(hull) * 4
m.view = wfViewCombat
m.setInfo("wf.msg.pirate_ambush")
case EncounterPoliceScan:
if m.ship.HasIllegalCargo() {
lost := m.ship.ConfiscateIllegalCargo()
m.setInfof("wf.msg.cargo_confiscated", formatWfCredits(lost))
} else {
m.setInfo("wf.msg.scan_clear")
}
m.view = wfViewHub
case EncounterDistress:
reward, wasTrap := m.ship.HelpDistressed(m.rng)
if wasTrap {
hull, laser := GeneratePirateStats(dest, m.rng)
m.combat = NewWayfarerCombat("wf.enemy.raider", hull, laser,
m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true)
m.combatRewardCredits = float64(hull) * 4
m.view = wfViewCombat
m.setInfo("wf.msg.distress_trap")
} else {
m.setInfof("wf.msg.distress_reward", formatWfCredits(reward))
m.view = wfViewHub
}
case EncounterDerelict:
found := m.ship.DerelictFind(m.rng)
m.setInfof("wf.msg.derelict_find", formatWfCredits(found))
m.view = wfViewHub
}
return m, nil
}
// --- Бой -------------------------------------------------------------------
func (m WayfarerModel) handleCombatKey(key string) (tea.Model, tea.Cmd) {
c := m.combat
if c == nil || c.Outcome != CombatOngoing {
return m, nil
}
switch key {
case "1":
c.Attack(m.rng)
case "2":
c.Flee(m.rng)
case "3":
if c.Negotiable {
c.Negotiate(m.rng)
}
default:
return m, nil
}
return m.resolveCombatIfDone()
}
func (m WayfarerModel) resolveCombatIfDone() (tea.Model, tea.Cmd) {
c := m.combat
switch c.Outcome {
case CombatWon:
m.ship.Hull = c.PlayerHull
m.ship.Shield = c.PlayerShield
m.ship.Kills++
m.ship.Credits += m.combatRewardCredits
m.setInfof("wf.msg.combat_won", formatWfCredits(m.combatRewardCredits))
m.view = wfViewHub
m.combat = nil
case CombatFled:
m.ship.Hull = c.PlayerHull
m.ship.Shield = c.PlayerShield
m.setInfo("wf.msg.combat_fled")
m.view = wfViewHub
m.combat = nil
case CombatNegotiated:
m.ship.Hull = c.PlayerHull
m.ship.Shield = c.PlayerShield
m.setInfo("wf.msg.combat_negotiated")
m.view = wfViewHub
m.combat = nil
case CombatLost:
m.ship.Hull = 0
if m.ship.Equipment[EquipEscapePod] {
m.ship.Equipment[EquipEscapePod] = false // одноразовое использование
m.ship.Hull = m.ship.MaxHull / 4
m.ship.Shield = 0
m.ship.Credits = m.ship.Credits / 2
for i := range m.ship.Cargo {
m.ship.Cargo[i] = 0
}
m.setInfo("wf.msg.escape_pod_used")
m.view = wfViewHub
m.combat = nil
} else {
m.view = wfViewGameOver
}
}
return m, nil
}
// --- Список квестов -----------------------------------------------------
// availableQuests возвращает квесты, которые ещё можно предложить
// игроку (не завершены и не провалены) — используется только для
// подсчёта в статусе и функцией RollQuestOffer, свободного выбора
// из этого списка больше нет (квесты теперь предлагаются случайно
// при прибытии в систему).
func (m WayfarerModel) availableQuests() []WayfarerQuestDef {
var out []WayfarerQuestDef
for _, q := range WayfarerQuestDefs {
if !m.ship.CompletedQuests[q.Key] && !m.ship.FailedQuests[q.Key] {
out = append(out, q)
}
}
return out
}
func (m WayfarerModel) handleQuestListKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "q", "esc":
m.view = wfViewHub
m.message = ""
case "enter", " ":
if m.ship.ActiveQuest != nil {
m.view = wfViewQuestStep
}
}
return m, nil
}
func (m WayfarerModel) handleQuestOfferKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "1", "enter", " ":
questKey := m.pendingQuestKey
m.pendingQuestKey = ""
if err := m.ship.StartQuest(questKey); err != nil {
m.setError(err)
m.view = wfViewHub
return m, nil
}
m.view = wfViewQuestStep
case "2", "q", "esc":
m.pendingQuestKey = ""
m.setInfo("wf.quests.declined")
m.view = wfViewHub
}
return m, nil
}
func (m WayfarerModel) viewQuestOffer() string {
var b strings.Builder
def, _ := FindWayfarerQuest(m.pendingQuestKey)
fmt.Fprintln(&b, titleStyle.Render(T("wf.quests.offer.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, menuCursorStyle.Render(T(def.TitleKey)))
fmt.Fprintln(&b, wfWrap(T(def.BriefKey), m.termWidth))
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.quests.offer.accept"))
fmt.Fprintln(&b, T("wf.quests.offer.decline"))
return b.String()
}
func (m WayfarerModel) handleQuestStepKey(key string) (tea.Model, tea.Cmd) {
step, ok := m.ship.CurrentQuestStep()
if !ok {
m.view = wfViewQuestList
m.cursor = 0
return m, nil
}
switch key {
case "q", "esc":
m.ship.AbandonQuest()
m.view = wfViewQuestList
m.cursor = 0
return m, nil
}
for i := 1; i <= 9 && i <= len(step.Choices); i++ {
if key == fmt.Sprintf("%d", i) {
choice, err := m.ship.ChooseQuestOption(i - 1)
if err != nil {
m.setError(err)
return m, nil
}
if choice.ResultKey != "" {
m.message = T(choice.ResultKey)
m.isError = false
}
if m.ship.ActiveQuest == nil {
m.view = wfViewQuestList
m.cursor = 0
}
return m, nil
}
}
return m, nil
}
// --- Экран окончания карьеры (гибель без спасательной капсулы) --------
func (m WayfarerModel) handleGameOverKey(key string) (tea.Model, tea.Cmd) {
switch key {
case "n":
_ = DeleteWayfarerSave()
m.ship = NewWayfarerShip(wayfarerStartSystem)
m.view = wfViewHub
m.message = ""
m.combat = nil
case "q":
_ = DeleteWayfarerSave()
m.backToMenu = true
}
return m, nil
}
// --- Форматирование -----------------------------------------------------
func formatWfCredits(v float64) string {
return fmt.Sprintf("%.0f", v)
}
// --- Отрисовка ------------------------------------------------------------
func (m WayfarerModel) View() string {
if m.quitting {
return T("common.goodbye")
}
if m.confirmingReset {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, errorStyle.Render(T("wf.confirm_reset")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("wf.confirm_reset.hint")))
return b.String()
}
switch m.view {
case wfViewMarket:
return m.viewMarket()
case wfViewShipyard:
return m.viewShipyard()
case wfViewTravel:
return m.viewTravel()
case wfViewCombat:
return m.viewCombat()
case wfViewQuestList:
return m.viewQuestList()
case wfViewQuestStep:
return m.viewQuestStep()
case wfViewQuestOffer:
return m.viewQuestOffer()
case wfViewGameOver:
return m.viewGameOver()
default:
return m.viewHub()
}
}
func (m WayfarerModel) renderShipStatus() string {
s := m.ship
return Tf("wf.status_line",
formatWfCredits(s.Credits), s.CargoUsed(), s.CargoCapacity,
s.Fuel, s.FuelCapacity, s.Hull, s.MaxHull, s.Shield, s.MaxShield)
}
// wfWrap переносит длинный текст (обычно — прозу квестов) по
// ширине реального терминала, чтобы строка не обрезалась и не
// вылезала за пределы экрана без переноса. Ширина ограничена
// разумным диапазоном: не уже 40 колонок (совсем узкие терминалы) и
// не шире 100 (сплошной текст на весь широкий терминал читать
// неудобно).
func wfWrap(text string, termWidth int) string {
width := termWidth - 4
if width < 40 {
width = 60 // разумный запасной вариант, пока реальный размер ещё не пришёл через WindowSizeMsg
}
if width > 100 {
width = 100
}
return lipgloss.NewStyle().Width(width).Render(text)
}
func (m WayfarerModel) renderMessage(b *strings.Builder) {
if m.message == "" {
return
}
fmt.Fprintln(b)
if m.isError {
fmt.Fprintln(b, errorStyle.Render(wfWrap("! "+m.message, m.termWidth)))
} else {
fmt.Fprintln(b, infoStyle.Render(wfWrap(m.message, m.termWidth)))
}
}
func (m WayfarerModel) viewHub() string {
sys := m.currentSystem()
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, Tf("wf.system_line", sys.Name, T(wayfarerGovernmentNameKeys[sys.Government]), sys.TechLevel))
fmt.Fprintln(&b, m.renderShipStatus())
fmt.Fprintln(&b, Tf("wf.rating_line", T(m.ship.PilotRating()), m.ship.Kills))
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.hub.hint1"))
fmt.Fprintln(&b, T("wf.hub.hint2"))
m.renderMessage(&b)
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("wf.hint.quit")))
return b.String()
}
func (m WayfarerModel) viewMarket() string {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.market.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderShipStatus())
fmt.Fprintln(&b)
for i, c := range WayfarerCommodities {
price := m.marketPrices[i]
line := Tf("wf.market.line", T(c.Key), price, m.ship.Cargo[i])
if m.ship.Cargo[i] > 0 {
line += Tf("wf.market.bought_at", m.ship.CargoCostBasis[i])
}
if c.Illegal {
line += " " + dimStyle.Render(T("wf.market.illegal_tag"))
}
if i == m.cursor {
line = menuCursorStyle.Render("> " + line)
} else {
line = " " + line
}
fmt.Fprintln(&b, line)
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.market.hint"))
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewShipyard() string {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.shipyard.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderShipStatus())
fmt.Fprintln(&b)
lines := []string{
Tf("wf.shipyard.refuel", (m.ship.FuelCapacity-m.ship.Fuel)*wayfarerRefuelPrice),
Tf("wf.shipyard.repair", float64(m.ship.MaxHull-m.ship.Hull)*wayfarerRepairPrice),
}
for _, eq := range wayfarerEquipmentOrder {
status := T("wf.shipyard.available")
if m.ship.Equipment[eq] {
status = T("wf.shipyard.owned")
}
lines = append(lines, Tf("wf.shipyard.equip_line", T(wayfarerEquipmentNameKey[eq]), wayfarerEquipmentCost[eq], status))
}
for i, line := range lines {
if i == m.cursor {
fmt.Fprintln(&b, menuCursorStyle.Render("> "+line))
} else {
fmt.Fprintln(&b, " "+line)
}
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.shipyard.hint"))
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewTravel() string {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.travel.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, Tf("wf.travel.range", m.ship.FuelRange()))
fmt.Fprintln(&b)
if len(m.travelCandidates) == 0 {
fmt.Fprintln(&b, dimStyle.Render(T("wf.travel.none_in_range")))
}
for i, id := range m.travelCandidates {
sys := GenerateSystem(id)
dist := m.currentSystem().Distance(sys)
line := Tf("wf.travel.line", sys.Name, T(wayfarerGovernmentNameKeys[sys.Government]), dist)
if i == m.cursor {
line = menuCursorStyle.Render("> " + line)
} else {
line = " " + line
}
fmt.Fprintln(&b, line)
}
galacticLine := T("wf.travel.galactic_jump")
if m.cursor == len(m.travelCandidates) {
galacticLine = menuCursorStyle.Render("> " + galacticLine)
} else {
galacticLine = " " + galacticLine
}
fmt.Fprintln(&b, galacticLine)
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.travel.hint"))
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewCombat() string {
c := m.combat
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.combat.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, Tf("wf.combat.enemy_line", T(c.EnemyNameKey), c.EnemyHull, c.EnemyMaxHull))
fmt.Fprintln(&b, Tf("wf.combat.player_line", c.PlayerHull, c.PlayerMaxHull, c.PlayerShield, c.PlayerMaxShield))
fmt.Fprintln(&b)
if c.Round > 0 {
fmt.Fprintln(&b, Tf("wf.combat.round_result", c.LastPlayerDamage, c.LastEnemyDamage))
}
switch c.Outcome {
case CombatOngoing:
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.combat.hint.attack"))
fmt.Fprintln(&b, T("wf.combat.hint.flee"))
if c.Negotiable {
fmt.Fprintln(&b, T("wf.combat.hint.negotiate"))
}
case CombatWon:
fmt.Fprintln(&b, winStyle.Render(T("wf.combat.outcome.won")))
case CombatFled:
fmt.Fprintln(&b, infoStyle.Render(T("wf.combat.outcome.fled")))
case CombatNegotiated:
fmt.Fprintln(&b, infoStyle.Render(T("wf.combat.outcome.negotiated")))
case CombatLost:
fmt.Fprintln(&b, errorStyle.Render(T("wf.combat.outcome.lost")))
}
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewQuestList() string {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.quests.title")))
fmt.Fprintln(&b)
if m.ship.ActiveQuest != nil {
def, _ := FindWayfarerQuest(m.ship.ActiveQuest.Key)
fmt.Fprintln(&b, Tf("wf.quests.active", T(def.TitleKey)))
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.quests.hint.continue"))
m.renderMessage(&b)
return b.String()
}
remaining := len(m.availableQuests())
done := len(WayfarerQuestDefs) - remaining
fmt.Fprintln(&b, dimStyle.Render(Tf("wf.quests.progress", done, len(WayfarerQuestDefs))))
fmt.Fprintln(&b)
if remaining == 0 {
fmt.Fprintln(&b, T("wf.quests.all_done"))
} else {
fmt.Fprintln(&b, T("wf.quests.none_active"))
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.quests.hint.list"))
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewQuestStep() string {
var b strings.Builder
def, _ := FindWayfarerQuest(m.ship.ActiveQuest.Key)
step, ok := m.ship.CurrentQuestStep()
fmt.Fprintln(&b, titleStyle.Render(T(def.TitleKey)))
fmt.Fprintln(&b)
if !ok {
m.renderMessage(&b)
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.quests.hint.back"))
return b.String()
}
fmt.Fprintln(&b, wfWrap(T(step.TextKey), m.termWidth))
fmt.Fprintln(&b)
for i, c := range step.Choices {
label := fmt.Sprintf("[%d] %s", i+1, T(c.LabelKey))
fmt.Fprintln(&b, wfWrap(label, m.termWidth))
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("wf.quests.hint.abandon"))
m.renderMessage(&b)
return b.String()
}
func (m WayfarerModel) viewGameOver() string {
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, errorStyle.Render(T("wf.gameover.destroyed")))
fmt.Fprintln(&b, Tf("wf.gameover.stats", T(m.ship.PilotRating()), m.ship.Kills))
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("wf.gameover.hint")))
return b.String()
}
// wayfarerGovernmentNameKeys — ключи перевода названий правительств
// (индекс = значение WayfarerGovernment).
var wayfarerGovernmentNameKeys = [...]string{
"wf.gov.anarchy", "wf.gov.feudal", "wf.gov.dictatorship", "wf.gov.communist",
"wf.gov.confederacy", "wf.gov.democracy", "wf.gov.corporate",
}