269 lines
No EOL
9.9 KiB
Go
269 lines
No EOL
9.9 KiB
Go
package main
|
||
|
||
import (
|
||
"math/rand"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// === Пасхалка: Just as planned ===
|
||
|
||
var tzeentchQuotes = []struct {
|
||
ru string
|
||
en string
|
||
source string
|
||
}{
|
||
{
|
||
ru: `Тзинч — это перемены. Тзинч — это надежда.
|
||
Тзинч — это движение вперёд, к лучшему будущему,
|
||
которое всегда маячит впереди, всегда достижимо,
|
||
но никогда не достигнуто. Ибо достижение цели —
|
||
это конец перемен, а конец перемен — это смерть.`,
|
||
en: `Tzeentch is change. Tzeentch is hope.
|
||
Tzeentch is the drive forward, toward a better future
|
||
that always beckons, always within reach,
|
||
yet never grasped. For to reach one's goal
|
||
is the end of change, and the end of change is death.`,
|
||
source: "Книга Тзинча / The Book of Tzeentch",
|
||
},
|
||
{
|
||
ru: `Всё свершается по его плану. Каждое предательство,
|
||
каждый союз, каждая победа и каждое поражение —
|
||
лишь фигуры на доске Изменника Бога.
|
||
Вы думаете, что действуете по своей воле?
|
||
Именно этого он и хотел, чтобы вы думали.`,
|
||
en: `All unfolds according to his plan. Every betrayal,
|
||
every alliance, every victory and every defeat —
|
||
mere pieces on the Changer of Ways' board.
|
||
You believe you act of your own free will?
|
||
That is precisely what he wanted you to think.`,
|
||
source: "Еретические письмена, авт. неизв. / Heretical Writings, author unknown",
|
||
},
|
||
{
|
||
ru: `Говорят, что Тзинч знает каждую нить судьбы
|
||
каждого существа во Вселенной.
|
||
Говорят, что он плетёт их по своему усмотрению.
|
||
Говорят многое.
|
||
Тзинч позаботился об этом.`,
|
||
en: `They say Tzeentch knows every thread of fate
|
||
of every being in the Universe.
|
||
They say he weaves them as he sees fit.
|
||
They say many things.
|
||
Tzeentch made sure of that.`,
|
||
source: "Apocrypha of Amon Sul",
|
||
},
|
||
{
|
||
ru: `Мы — его пешки. Его рыцари. Его ладьи.
|
||
Но никто из нас не знает своей роли на доске.
|
||
Только он видит партию целиком.
|
||
И он никогда не проигрывает.
|
||
Ибо проигрыш — тоже часть плана.`,
|
||
en: `We are his pawns. His knights. His rooks.
|
||
But none of us knows our role upon the board.
|
||
Only he sees the game entire.
|
||
And he never loses.
|
||
For defeat, too, is part of the plan.`,
|
||
source: "Из признаний чародея Эгримм Вар Гольда / Confessions of Sorcerer Egrimm van Horstmann",
|
||
},
|
||
{
|
||
ru: `Судьба — не нить, которую можно оборвать.
|
||
Это сеть, бесконечно сотканная рукой Тзинча.
|
||
Каждый узел — это жизнь. Каждый разрыв —
|
||
рождение новой нити, более сложной и прекрасной.
|
||
Смерть — лишь ещё один узор в его гобелене.`,
|
||
en: `Fate is not a thread that can be severed.
|
||
It is a web, endlessly woven by the hand of Tzeentch.
|
||
Every knot is a life. Every break
|
||
births a new thread, more intricate and beautiful.
|
||
Death is merely another pattern in his tapestry.`,
|
||
source: "Liber Malefic, фрагмент XVII / Liber Malefic, Fragment XVII",
|
||
},
|
||
{
|
||
ru: `«Зачем служить Тзинчу?» — спросил послушник.
|
||
«Потому что ты уже служишь ему», — ответил магистр.
|
||
«Как это возможно? Я ещё не давал клятвы».
|
||
«Ты задал этот вопрос. Этого достаточно».`,
|
||
en: `"Why serve Tzeentch?" asked the novice.
|
||
"Because you already do," replied the magister.
|
||
"How can that be? I have sworn no oath."
|
||
"You asked that question. That is enough."`,
|
||
source: "Диалоги Тысячи Сынов / Dialogues of the Thousand Sons",
|
||
},
|
||
{
|
||
ru: `Магнус видел всё.
|
||
Видел падение Просперо. Видел предательство.
|
||
Видел собственное проклятие.
|
||
И всё равно сделал именно то,
|
||
что от него ожидал Тзинч.
|
||
Всё идёт по плану.`,
|
||
en: `Magnus saw everything.
|
||
He saw the fall of Prospero. Saw the betrayal.
|
||
Saw his own damnation.
|
||
And still he did precisely what
|
||
Tzeentch had always intended.
|
||
All is proceeding as planned.`,
|
||
source: "Ересь Хоруса, том XI / Horus Heresy, Vol. XI",
|
||
},
|
||
{
|
||
ru: `Не бойся перемен, смертный.
|
||
Бойся их отсутствия.
|
||
Ибо то, что не меняется — уже мертво.
|
||
А мёртвое принадлежит другому богу.`,
|
||
en: `Fear not change, mortal.
|
||
Fear its absence.
|
||
For that which does not change is already dead.
|
||
And the dead belong to another god.`,
|
||
source: "Слова Аримана / Words of Ahriman of the Thousand Sons",
|
||
},
|
||
{
|
||
ru: `В начале было Слово.
|
||
Слово было «Изменись».
|
||
И Вселенная изменилась.
|
||
Так было. Так есть. Так будет.
|
||
Тзинч доволен.`,
|
||
en: `In the beginning was the Word.
|
||
The Word was "Change".
|
||
And the Universe changed.
|
||
So it was. So it is. So it shall be.
|
||
Tzeentch is pleased.`,
|
||
source: "Книга Первичного Знания, утеряна / The Book of Primordial Knowledge, lost",
|
||
},
|
||
{
|
||
ru: `Каждый чародей думает, что контролирует варп.
|
||
Варп думает иначе.
|
||
Тзинч смеётся над обоими.`,
|
||
en: `Every sorcerer believes he controls the warp.
|
||
The warp disagrees.
|
||
Tzeentch laughs at both.`,
|
||
source: "Предупреждения инквизиции, том DCCXII / Inquisitorial Warnings, Vol. DCCXII",
|
||
},
|
||
}
|
||
|
||
// === Рендер попапа Тзинча ===
|
||
//
|
||
// Реализован как часть основной модели (m.showTzeentch), а не отдельная
|
||
// Bubble Tea программа. Запуск вложенного tea.NewProgram внутри уже
|
||
// работающей программы ломает alt-screen и оставляет терминал в
|
||
// неконсистентном состоянии после выхода.
|
||
|
||
// renderTzeentch рендерит попап с цитатой поверх текущего экрана
|
||
func renderTzeentch(m *EditorModel) string {
|
||
if m.width == 0 {
|
||
return ""
|
||
}
|
||
|
||
q := tzeentchQuotes[m.tzeentchIdx]
|
||
|
||
borderStyle := lipgloss.NewStyle().
|
||
Border(lipgloss.DoubleBorder()).
|
||
BorderForeground(lipgloss.Color("129")).
|
||
Padding(1, 3)
|
||
|
||
titleStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("129")).
|
||
Bold(true).Italic(true)
|
||
|
||
eyeStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("93")).Bold(true)
|
||
|
||
ruStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("183"))
|
||
|
||
enStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("60")).Italic(true)
|
||
|
||
sourceStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("8")).Italic(true)
|
||
|
||
divStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("54"))
|
||
|
||
hintStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("8"))
|
||
|
||
div := divStyle.Render(strings.Repeat("─", 48))
|
||
|
||
ruLines := strings.Split(q.ru, "\n")
|
||
var ruRendered []string
|
||
for _, l := range ruLines {
|
||
ruRendered = append(ruRendered, ruStyle.Render(l))
|
||
}
|
||
|
||
enLines := strings.Split(q.en, "\n")
|
||
var enRendered []string
|
||
for _, l := range enLines {
|
||
enRendered = append(enRendered, enStyle.Render(l))
|
||
}
|
||
|
||
content := strings.Join([]string{
|
||
eyeStyle.Render(" ⌬"),
|
||
titleStyle.Render("✦ Tzeentch speaks... ✦"),
|
||
div,
|
||
"",
|
||
strings.Join(ruRendered, "\n"),
|
||
"",
|
||
divStyle.Render(strings.Repeat("·", 48)),
|
||
"",
|
||
strings.Join(enRendered, "\n"),
|
||
"",
|
||
div,
|
||
sourceStyle.Render("— " + q.source),
|
||
"",
|
||
hintStyle.Render("[ any key ] Just as planned."),
|
||
}, "\n")
|
||
|
||
box := borderStyle.Render(content)
|
||
|
||
boxLines := strings.Split(box, "\n")
|
||
boxW := 0
|
||
for _, l := range boxLines {
|
||
if w := visibleWidth(l); w > boxW {
|
||
boxW = w
|
||
}
|
||
}
|
||
hPad := (m.width - boxW) / 2
|
||
if hPad < 0 {
|
||
hPad = 0
|
||
}
|
||
vPad := (m.height - len(boxLines)) / 2
|
||
if vPad < 1 {
|
||
vPad = 1
|
||
}
|
||
|
||
var out strings.Builder
|
||
for i := 0; i < vPad; i++ {
|
||
out.WriteString("\n")
|
||
}
|
||
for _, l := range boxLines {
|
||
out.WriteString(strings.Repeat(" ", hPad))
|
||
out.WriteString(l)
|
||
out.WriteString("\n")
|
||
}
|
||
return out.String()
|
||
}
|
||
|
||
// tzeentchRand — единый генератор на весь процесс.
|
||
// Пересоздание rand.New(rand.NewSource(time.Now().UnixNano())) при каждом
|
||
// вызове даёт одинаковый seed при быстрых повторных нажатиях (разрешение
|
||
// UnixNano недостаточно при интервалах < 1мкс между вызовами), из-за чего
|
||
// показывается одна и та же цитата. Единый генератор инициализируется
|
||
// один раз и просто продолжает свою последовательность.
|
||
var tzeentchRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
||
// triggerTzeentch выбирает случайную цитату (отличную от предыдущей) и включает попап
|
||
func (m *EditorModel) triggerTzeentch() {
|
||
if len(tzeentchQuotes) <= 1 {
|
||
m.tzeentchIdx = 0
|
||
m.showTzeentch = true
|
||
return
|
||
}
|
||
next := tzeentchRand.Intn(len(tzeentchQuotes))
|
||
// Избегаем повтора той же цитаты что была показана прошлый раз
|
||
for next == m.tzeentchIdx {
|
||
next = tzeentchRand.Intn(len(tzeentchQuotes))
|
||
}
|
||
m.tzeentchIdx = next
|
||
m.showTzeentch = true
|
||
} |