grimoir/tzeentch.go
2026-07-04 13:04:33 +03:00

269 lines
No EOL
9.9 KiB
Go
Raw Permalink 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 (
"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
}