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 // ключ квеста, предложенного встречей в пути, но ещё не принятого/отклонённого 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())) if ship, err := LoadWayfarerShip(); err == nil { return WayfarerModel{ship: ship, view: wfViewHub, rng: rng, termWidth: 80} } return WayfarerModel{ship: NewWayfarerShip(wayfarerStartSystem), view: wfViewHub, rng: rng, termWidth: 80} } 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) } 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 } 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 = "" } 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": sys := m.currentSystem() price := CommodityPrice(m.cursor, sys, m.rng) 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.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 } 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") } 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 { sys := m.currentSystem() 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 := CommodityPrice(i, sys, m.rng) 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", }