From 4379e1a092f387614861bee6afd916a2e2b3a511 Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Tue, 7 Jul 2026 09:58:26 +0300 Subject: [PATCH] Added new features --- corpsestarch_game.go | 98 ++++++++++++++++++++++++++- corpsestarch_game_test.go | 139 +++++++++++++++++++++++++++++++++++++- corpsestarch_tui.go | 34 ++++++++-- corpsestarch_tui_test.go | 51 +++++++++++++- ggc | 4 +- ggc.exe | 4 +- rules.go | 28 +++++++- translations.go | 20 ++++-- 8 files changed, 358 insertions(+), 20 deletions(-) diff --git a/corpsestarch_game.go b/corpsestarch_game.go index 59d8059..5f3bcb6 100644 --- a/corpsestarch_game.go +++ b/corpsestarch_game.go @@ -59,6 +59,21 @@ const ( corpseStarchClickUpgradeGain = 1 ) +// Параметры варп-выброса: при достижении этого порога Скверна +// "прорывается" наружу — половина текущего запаса крахмала и +// половина каждого тира сервиторов теряется, Скверна сбрасывается в +// 0, и начинается вторжение хаоситов на фиксированное реальное +// время. Пока оно идёт, сервиторы дают заметно меньше дохода — часть +// сил станции отвлечена на оборону — а активная защита (повторные +// нажатия) даёт шанс на бонус по окончании вторжения. +const ( + corpseStarchCorruptionBreachThreshold = 3 + corpseStarchIncursionDuration = 1 * time.Hour + corpseStarchIncursionYieldMultiplier = 0.5 + corpseStarchDefendClicksForBonus = 10 + corpseStarchDefendBonusReward = 200.0 +) + // corpseStarchMission — активная (запущенная) миссия. type corpseStarchMission struct { DefIdx int @@ -77,6 +92,13 @@ type CorpseStarchGameState struct { Corruption int + // ChaosIncursionUntil — если не нулевое время и ещё не наступило, + // идёт вторжение хаоситов после варп-выброса (см. triggerWarpBreach). + // ChaosDefendClicks считает, сколько раз игрок активно отбивался + // за текущее вторжение — от этого зависит бонус по его окончании. + ChaosIncursionUntil time.Time + ChaosDefendClicks int + ActiveMission *corpseStarchMission MissionsDone int @@ -140,13 +162,29 @@ func (g *CorpseStarchGameState) Tick(now time.Time) { elapsed := now.Sub(g.LastTick).Seconds() g.LastTick = now if elapsed <= 0 { + g.checkIncursionResolution(now) return } var perSec float64 for i, def := range corpseStarchServitorDefs { perSec += float64(g.ServitorCounts[i]) * def.YieldPerSec } + if g.InChaosIncursion(now) { + perSec *= corpseStarchIncursionYieldMultiplier + } g.earn(perSec * elapsed) + g.checkIncursionResolution(now) +} + +// checkIncursionResolution проверяет, не истекло ли вторжение +// хаоситов, и если да — разрешает его (см. resolveChaosIncursion). +// Идемпотентно: повторные вызовы после разрешения ничего не делают, +// пока не начнётся новое вторжение. +func (g *CorpseStarchGameState) checkIncursionResolution(now time.Time) { + if g.ChaosIncursionUntil.IsZero() || now.Before(g.ChaosIncursionUntil) { + return + } + g.resolveChaosIncursion() } // servitorCost возвращает стоимость следующей покупки тира idx. @@ -231,10 +269,68 @@ func (g *CorpseStarchGameState) DipChainsword(now time.Time, roll float64) Corps penalty := g.Starch * 0.1 g.Starch -= penalty g.addLog("corpsestarch.log.dip_corruption") + if g.Corruption >= corpseStarchCorruptionBreachThreshold { + g.triggerWarpBreach(now) + } return DipCorruption } } +// triggerWarpBreach — Скверна прорывается варп-выбросом: половина +// текущего запаса крахмала и половина каждого тира сервиторов +// теряется, Скверна сбрасывается в 0 (выброс её "выпустил"), и +// начинается вторжение хаоситов на фиксированное реальное время. +func (g *CorpseStarchGameState) triggerWarpBreach(now time.Time) { + g.Starch *= 0.5 + for i := range g.ServitorCounts { + g.ServitorCounts[i] /= 2 + } + g.Corruption = 0 + g.ChaosIncursionUntil = now.Add(corpseStarchIncursionDuration) + g.ChaosDefendClicks = 0 + g.addLog("corpsestarch.log.warp_breach") +} + +// InChaosIncursion — идёт ли прямо сейчас вторжение хаоситов после +// варп-выброса. +func (g *CorpseStarchGameState) InChaosIncursion(now time.Time) bool { + return !g.ChaosIncursionUntil.IsZero() && now.Before(g.ChaosIncursionUntil) +} + +// ChaosIncursionRemaining — сколько реального времени осталось до +// конца вторжения (0, если вторжения сейчас нет). +func (g *CorpseStarchGameState) ChaosIncursionRemaining(now time.Time) time.Duration { + if !g.InChaosIncursion(now) { + return 0 + } + return g.ChaosIncursionUntil.Sub(now) +} + +// DefendAgainstChaos — активная защита от хаоситов (повторные +// нажатия во время вторжения). Возвращает false, если вторжения +// сейчас нет — действие недоступно вне его рамок. +func (g *CorpseStarchGameState) DefendAgainstChaos(now time.Time) bool { + if !g.InChaosIncursion(now) { + return false + } + g.ChaosDefendClicks++ + return true +} + +// resolveChaosIncursion завершает текущее вторжение: если игрок +// набрал достаточно активных защит за время вторжения — начисляет +// бонус в благодарность за стойкость станции. +func (g *CorpseStarchGameState) resolveChaosIncursion() { + if g.ChaosDefendClicks >= corpseStarchDefendClicksForBonus { + g.earn(corpseStarchDefendBonusReward) + g.addLog("corpsestarch.log.incursion_repelled_bonus") + } else { + g.addLog("corpsestarch.log.incursion_repelled") + } + g.ChaosIncursionUntil = time.Time{} + g.ChaosDefendClicks = 0 +} + // StartMission запускает миссию defIdx, если она открыта и сейчас // нет другой активной миссии. func (g *CorpseStarchGameState) StartMission(now time.Time, defIdx int) bool { @@ -309,4 +405,4 @@ func (g *CorpseStarchGameState) ServitorUnlocked(idx int) bool { // MissionUnlocked проверяет, видна ли конкретная миссия idx. func (g *CorpseStarchGameState) MissionUnlocked(idx int) bool { return g.TotalEarned >= corpseStarchMissionDefs[idx].UnlockAt -} +} \ No newline at end of file diff --git a/corpsestarch_game_test.go b/corpsestarch_game_test.go index ae759e9..ad059e1 100644 --- a/corpsestarch_game_test.go +++ b/corpsestarch_game_test.go @@ -178,6 +178,143 @@ func TestDipChainswordCorruptionOutcome(t *testing.T) { } } +func TestWarpBreachTriggersAtCorruptionThreshold(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.Starch = 1000 + g.TotalEarned = 1000 + g.ServitorCounts[0] = 10 + g.ServitorCounts[1] = 4 + + // два неудачных окунания — Скверна 1, потом 2, выброс ещё не должен случиться + g.DipChainsword(csT(0), 0.95) + g.DipChainsword(csT(1), 0.95) + if g.Corruption != 2 { + t.Fatalf("ожидалась Скверна 2 после двух неудач, получено %d", g.Corruption) + } + if g.InChaosIncursion(csT(1)) { + t.Fatalf("вторжение не должно было начаться раньше порога") + } + + // третье неудачное окунание достигает порога — должен случиться выброс + starchBefore := g.Starch + g.DipChainsword(csT(2), 0.95) + + if g.Corruption != 0 { + t.Errorf("Скверна должна была сброситься в 0 после выброса, получено %d", g.Corruption) + } + if !g.InChaosIncursion(csT(2)) { + t.Fatalf("вторжение хаоситов должно было начаться сразу после выброса") + } + // после выброса запас должен был уменьшиться примерно вдвое от + // того, что было ДО штрафа за само неудачное окунание (10%), — + // проверяем лишь то, что итог заметно меньше половины исходного + // запаса до всей цепочки событий, без привязки к точной формуле + if g.Starch >= starchBefore { + t.Errorf("запас должен был заметно уменьшиться при выбросе, было %v, стало %v", starchBefore, g.Starch) + } + if g.ServitorCounts[0] != 5 || g.ServitorCounts[1] != 2 { + t.Errorf("количество сервиторов должно было уполовиниться (10->5, 4->2), получено %v", g.ServitorCounts) + } +} + +func TestWarpBreachHalvesServitorsWithFloorDivision(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.ServitorCounts[0] = 7 // нечётное число — проверяем округление вниз + g.Corruption = corpseStarchCorruptionBreachThreshold - 1 + g.DipChainsword(csT(0), 0.95) // добивает Скверну до порога + + if g.ServitorCounts[0] != 3 { + t.Errorf("7 сервиторов должны были уполовиниться с округлением вниз до 3, получено %d", g.ServitorCounts[0]) + } +} + +func TestChaosIncursionReducesServitorYield(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.ServitorCounts[0] = 10 // 10 * 0.5/сек = 5/сек в обычном режиме + // ChaosIncursionUntil остаётся нулевым — вторжения нет вообще + g.Tick(csT(10)) + normalEarn := g.Starch + + g2 := NewCorpseStarchGame(csT(0)) + g2.ServitorCounts[0] = 10 + g2.ChaosIncursionUntil = csT(100) // вторжение идёт весь этот тик (10 < 100) + g2.Tick(csT(10)) + + if g2.Starch >= normalEarn { + t.Errorf("доход во время вторжения должен быть меньше обычного: обычный %v, во время вторжения %v", normalEarn, g2.Starch) + } + if g2.Starch != normalEarn*corpseStarchIncursionYieldMultiplier { + t.Errorf("ожидался ровно множитель %v от обычного дохода, получено %v (обычный: %v)", + corpseStarchIncursionYieldMultiplier, g2.Starch, normalEarn) + } +} + +func TestDefendAgainstChaosOnlyWorksDuringIncursion(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + if g.DefendAgainstChaos(csT(0)) { + t.Errorf("защита не должна была сработать без активного вторжения") + } + if g.ChaosDefendClicks != 0 { + t.Errorf("счётчик защиты не должен был увеличиться вне вторжения") + } + + g.ChaosIncursionUntil = csT(100) + if !g.DefendAgainstChaos(csT(0)) { + t.Errorf("защита должна была сработать во время вторжения") + } + if g.ChaosDefendClicks != 1 { + t.Errorf("ожидался счётчик защиты 1, получено %d", g.ChaosDefendClicks) + } +} + +func TestIncursionResolvesAfterDurationWithoutBonus(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.ChaosIncursionUntil = csT(10) + g.ChaosDefendClicks = corpseStarchDefendClicksForBonus - 1 // недостаточно для бонуса + starchBefore := g.Starch + + g.Tick(csT(11)) // время вышло за отметку окончания вторжения + + if g.InChaosIncursion(csT(11)) { + t.Errorf("вторжение должно было завершиться") + } + if !g.ChaosIncursionUntil.IsZero() { + t.Errorf("ChaosIncursionUntil должен был сброситься в нулевое значение") + } + if g.ChaosDefendClicks != 0 { + t.Errorf("счётчик защиты должен был сброситься после разрешения вторжения") + } + if g.Starch != starchBefore { + t.Errorf("без достаточной защиты бонус не должен был начислиться, было %v, стало %v", starchBefore, g.Starch) + } +} + +func TestIncursionResolvesWithBonusWhenDefendedEnough(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.ChaosIncursionUntil = csT(10) + g.ChaosDefendClicks = corpseStarchDefendClicksForBonus + starchBefore := g.Starch + + g.Tick(csT(11)) + + if g.Starch != starchBefore+corpseStarchDefendBonusReward { + t.Errorf("ожидался бонус %v за успешную защиту, было %v, стало %v", + corpseStarchDefendBonusReward, starchBefore, g.Starch) + } +} + +func TestChaosIncursionRemainingCountdown(t *testing.T) { + g := NewCorpseStarchGame(csT(0)) + g.ChaosIncursionUntil = csT(100) + remaining := g.ChaosIncursionRemaining(csT(40)) + if remaining != 60*time.Second { + t.Errorf("ожидалось 60 секунд до конца вторжения, получено %v", remaining) + } + if g.ChaosIncursionRemaining(csT(200)) != 0 { + t.Errorf("после окончания вторжения обратный отсчёт должен быть 0") + } +} + func TestStartMissionRequiresUnlockAndNoActiveMission(t *testing.T) { g := NewCorpseStarchGame(csT(0)) if g.StartMission(csT(0), 0) { @@ -232,4 +369,4 @@ func TestLogTrimsToLimit(t *testing.T) { if len(g.Log) != corpseStarchLogLimit { t.Errorf("лог должен обрезаться до лимита %d, получено %d", corpseStarchLogLimit, len(g.Log)) } -} +} \ No newline at end of file diff --git a/corpsestarch_tui.go b/corpsestarch_tui.go index 310fa07..19f9bbe 100644 --- a/corpsestarch_tui.go +++ b/corpsestarch_tui.go @@ -136,12 +136,26 @@ func (m CorpseStarchModel) buildActions(now time.Time) []corpseStarchAction { case DipNothing: m.setInfo("corpsestarch.log.dip_nothing") case DipCorruption: - m.setInfo("corpsestarch.log.dip_corruption") + if m.game.InChaosIncursion(now) { + m.setInfo("corpsestarch.log.warp_breach") + } else { + m.setInfo("corpsestarch.log.dip_corruption") + } } }, }) } + if g.InChaosIncursion(now) { + actions = append(actions, corpseStarchAction{ + Label: Tf("corpsestarch.action.defend", g.ChaosDefendClicks), + Handle: func(m *CorpseStarchModel, now time.Time) { + m.game.DefendAgainstChaos(now) + m.setInfo("corpsestarch.msg.defended") + }, + }) + } + if g.MissionsUnlocked() { if g.ActiveMission == nil { for i, def := range corpseStarchMissionDefs { @@ -243,12 +257,17 @@ func formatStarch(v float64) string { func formatDuration(d time.Duration) string { d = d.Round(time.Second) - m := int(d.Minutes()) + h := int(d.Hours()) + m := int(d.Minutes()) % 60 s := int(d.Seconds()) % 60 - if m > 0 { + switch { + case h > 0: + return fmt.Sprintf("%dh%02dm%02ds", h, m, s) + case m > 0: return fmt.Sprintf("%dm%02ds", m, s) + default: + return fmt.Sprintf("%ds", s) } - return fmt.Sprintf("%ds", s) } func (m CorpseStarchModel) View() string { @@ -276,6 +295,11 @@ func (m CorpseStarchModel) View() string { } fmt.Fprintln(&b) + if g.InChaosIncursion(now) { + fmt.Fprintln(&b, errorStyle.Render(Tf("corpsestarch.incursion.active", formatDuration(g.ChaosIncursionRemaining(now))))) + fmt.Fprintln(&b) + } + if g.MissionsUnlocked() && g.ActiveMission != nil { def := corpseStarchMissionDefs[g.ActiveMission.DefIdx] remaining := g.MissionRemaining(now) @@ -311,4 +335,4 @@ func (m CorpseStarchModel) View() string { fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.hint"))) return b.String() -} +} \ No newline at end of file diff --git a/corpsestarch_tui_test.go b/corpsestarch_tui_test.go index ddb79ec..c8769bc 100644 --- a/corpsestarch_tui_test.go +++ b/corpsestarch_tui_test.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "strings" "testing" "time" ) @@ -15,6 +17,53 @@ func TestCorpseStarchModelRequisitionViaKey(t *testing.T) { } } +func TestCorpseStarchModelDefendActionAppearsDuringIncursion(t *testing.T) { + m := NewCorpseStarchModel() + before := len(m.buildActions(m.game.LastTick)) + + m.game.ChaosIncursionUntil = m.game.LastTick.Add(1) + after := m.buildActions(m.game.LastTick) + if len(after) != before+1 { + t.Fatalf("ожидалось на одно действие больше во время вторжения, было %d, стало %d", before, len(after)) + } + if after[len(after)-1].Label != Tf("corpsestarch.action.defend", 0) { + t.Errorf("последнее действие должно быть 'отбиваться', получено: %q", after[len(after)-1].Label) + } +} + +func TestCorpseStarchModelDefendActionIncrementsCounterViaKey(t *testing.T) { + m := NewCorpseStarchModel() + m.game.ChaosIncursionUntil = time.Now().Add(time.Hour) + actions := m.buildActions(time.Now()) + defendKey := fmt.Sprintf("%d", len(actions)) + + next, _ := m.Update(key(defendKey)) + m = next.(CorpseStarchModel) + if m.game.ChaosDefendClicks != 1 { + t.Errorf("ожидался счётчик защиты 1 после нажатия, получено %d", m.game.ChaosDefendClicks) + } +} + +func TestCorpseStarchModelWarpBreachVisibleInView(t *testing.T) { + m := NewCorpseStarchModel() + m.game.Starch = 1000 + m.game.ServitorCounts[0] = 10 + m.game.DipChainsword(m.game.LastTick, 0.95) + m.game.DipChainsword(m.game.LastTick, 0.95) + m.game.DipChainsword(m.game.LastTick, 0.95) + + view := m.View() + if !strings.Contains(view, T("corpsestarch.log.warp_breach")) { + t.Errorf("экран должен был показать сообщение о варп-выбросе в журнале") + } + if !strings.Contains(view, "ВТОРЖЕНИЕ ХАОСА") { + t.Errorf("экран должен был показать баннер активного вторжения") + } + if m.game.ServitorCounts[0] != 5 { + t.Errorf("сервиторы должны были уполовиниться, получено %d", m.game.ServitorCounts[0]) + } +} + func TestCorpseStarchModelActionsExpandAsUnlocked(t *testing.T) { withTempConfigDir(t) m := NewCorpseStarchModel() @@ -183,4 +232,4 @@ func TestCorpseStarchModelMissionFlowViaKeys(t *testing.T) { if m.game.MissionsDone != 1 { t.Errorf("счётчик завершённых миссий должен был увеличиться") } -} +} \ No newline at end of file diff --git a/ggc b/ggc index ae66f0b..29f62a3 100755 --- a/ggc +++ b/ggc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a50888a20892259585dfe970d15b07937cbe28188d8851582938a0c20d118fd -size 4636834 +oid sha256:56746347f77fe0db636db25e7494ba43b5b4877d682f7dcd71f5e91ba4b0f446 +size 4649122 diff --git a/ggc.exe b/ggc.exe index 6cd7796..7e24922 100755 --- a/ggc.exe +++ b/ggc.exe @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:847c4cd717d22bff5753c00815e0fc2613d87523911a8e9279fab7bd112c1277 -size 4918272 +oid sha256:c7d822b3bb1bf3c4b8647c7a2d44b678c41baa09e7e47343f6b9d91f7d7bc911 +size 4928512 diff --git a/rules.go b/rules.go index 55ff758..ffb933e 100644 --- a/rules.go +++ b/rules.go @@ -1121,6 +1121,19 @@ const corpseStarchRulesRU = `ТРУПНЫЕ БАТОНЧИКИ (CORPSE-STARCH BO (от 20 секунд до нескольких минут); по завершении можно забрать награду. +СКВЕРНА И ВАРП-ВЫБРОС + Каждое неудачное окунание меча в отстойник добавляет единицу + Скверны. При достижении 3 она прорывается варп-выбросом: половина + текущего запаса крахмала и половина каждого тира сервиторов + теряется, Скверна сбрасывается в 0, и начинается вторжение + культистов Хаоса на целый час реального времени. Пока оно идёт, + сервиторы дают вдвое меньше дохода — часть сил станции отвлечена на + оборону. Появляется отдельное действие "отбиваться от культистов" — + каждое нажатие засчитывается; если за время вторжения накопится + достаточно активных защит, по его окончании начисляется бонус + крахмала в благодарность за стойкость станции. Цикл может + повториться — Скверна снова начинает копиться с нуля. + УПРАВЛЕНИЕ 1-9 выбрать пронумерованное действие (список меняется по мере открытия новых разделов — номера могут "переезжать") @@ -1167,6 +1180,19 @@ SECTIONS THAT UNLOCK AS YOU GATHER MORE time (from 20 seconds to a few minutes); collect the reward once it's done. +CORRUPTION AND WARP BREACH + Every unlucky chainsword dip adds one point of Corruption. Once it + reaches 3, it erupts in a warp breach: half your current starch + stockpile and half of every servitor tier are lost, Corruption + resets to 0, and a Chaos cultist incursion begins for a full hour + of real time. While it's ongoing, servitors yield only half their + usual income — part of the station's strength is diverted to + defense. A separate "fight off the cultists" action appears — each + press counts toward your defense; if you rack up enough active + defenses during the incursion, a bonus shipment of starch is + awarded once it ends, as thanks for the station's resolve. The + cycle can repeat — Corruption starts building up from zero again. + CONTROLS 1-9 choose a numbered action (the list grows as you unlock more sections — numbers may shift around) @@ -1659,4 +1685,4 @@ func wayfarerRules() string { return wayfarerRulesEN } return wayfarerRulesRU -} +} \ No newline at end of file diff --git a/translations.go b/translations.go index 08c5bb0..6300164 100644 --- a/translations.go +++ b/translations.go @@ -321,22 +321,28 @@ func init() { "corpsestarch.action.dip": {"Окунуть цепной меч в отстойник", "Dip your chainsword in the sump"}, "corpsestarch.action.start_mission": {"Начать зачистку: %s (%s)", "Start purge: %s (%s)"}, "corpsestarch.action.collect_mission": {"Забрать награду за зачистку", "Collect purge reward"}, + "corpsestarch.action.defend": {"Отбиваться от культистов Хаоса (заявлено атак: %d)", "Fight off Chaos cultists (attacks logged: %d)"}, "corpsestarch.msg.requisitioned": {"Реквизиция одобрена Муниторумом.", "Requisition approved by the Munitorum."}, "corpsestarch.msg.servitor_bought": {"Новая единица введена в строй.", "A new unit has been brought online."}, "corpsestarch.msg.upgrade_bought": {"Цепной меч заточен острее.", "The chainsword is sharper now."}, "corpsestarch.msg.cannot_afford": {"Недостаточно трупного крахмала.", "Not enough corpse starch."}, "corpsestarch.msg.mission_started": {"Отряд отправлен на зачистку.", "A squad has been dispatched on the purge."}, + "corpsestarch.msg.defended": {"Вы отбили атаку культистов.", "You fought off a cultist attack."}, "corpsestarch.mission.inprogress": {"Зачистка идёт: %s (осталось %s)", "Purge underway: %s (%s remaining)"}, "corpsestarch.mission.ready": {"Зачистка завершена: %s — заберите награду!", "Purge complete: %s — collect your reward!"}, - "corpsestarch.log.title": {"Журнал Муниторума:", "Munitorum log:"}, - "corpsestarch.log.dip_bonus": {"Отстойник выплюнул горсть трупного крахмала.", "The sump spat out a handful of corpse starch."}, - "corpsestarch.log.dip_blessing": {"Дух машины благословил ваш клинок — временный прирост дохода.", "The machine spirit blessed your blade — temporary income boost."}, - "corpsestarch.log.dip_nothing": {"Отстойник ответил тишиной.", "The sump answers only with silence."}, - "corpsestarch.log.dip_corruption": {"Скверна коснулась вашего снаряжения.", "Taint has touched your wargear."}, - "corpsestarch.log.mission_done": {"Отряд вернулся с зачистки с трофеями.", "The squad returned from the purge with spoils."}, + "corpsestarch.log.title": {"Журнал Муниторума:", "Munitorum log:"}, + "corpsestarch.log.dip_bonus": {"Отстойник выплюнул горсть трупного крахмала.", "The sump spat out a handful of corpse starch."}, + "corpsestarch.log.dip_blessing": {"Дух машины благословил ваш клинок — временный прирост дохода.", "The machine spirit blessed your blade — temporary income boost."}, + "corpsestarch.log.dip_nothing": {"Отстойник ответил тишиной.", "The sump answers only with silence."}, + "corpsestarch.log.dip_corruption": {"Скверна коснулась вашего снаряжения.", "Taint has touched your wargear."}, + "corpsestarch.incursion.active": {"ВТОРЖЕНИЕ ХАОСА! Культисты атакуют станцию — осталось: %s", "CHAOS INCURSION! Cultists are attacking the station — time left: %s"}, + "corpsestarch.log.warp_breach": {"Скверна прорвалась варп-выбросом! Половина запасов и оборудования потеряна — из пробоя хлынули культисты Хаоса.", "The taint erupts in a warp breach! Half your stockpile and equipment are lost — Chaos cultists pour through the rupture."}, + "corpsestarch.log.incursion_repelled": {"Вторжение культистов отражено. Обошлось малой кровью.", "The cultist incursion has been repelled. It could have been worse."}, + "corpsestarch.log.incursion_repelled_bonus": {"Вторжение отражено! За стойкость станция получила дополнительный груз трупного крахмала.", "The incursion has been repelled! For your resolve, the station received a bonus shipment of corpse starch."}, + "corpsestarch.log.mission_done": {"Отряд вернулся с зачистки с трофеями.", "The squad returned from the purge with spoils."}, "corpsestarch.servitor.skull": {"Сервочерп", "Servo-Skull"}, "corpsestarch.servitor.menial": {"Сервитор-подёнщик", "Menial Servitor"}, @@ -407,4 +413,4 @@ func init() { } { translations[k] = v } -} +} \ No newline at end of file