519 lines
25 KiB
Go
519 lines
25 KiB
Go
package main
|
||
|
||
import (
|
||
"math/rand"
|
||
"time"
|
||
)
|
||
|
||
// Режим истории: набор ресурсов, по одному на каждую "обычную"
|
||
// (с чётким исходом партии) игру коллекции. Победа в игре
|
||
// начисляет +1 её ресурса — но не более StoryDailyGrantsLimit раз в
|
||
// реальные сутки (скользящие 24 часа, как в Трупных батончиках —
|
||
// без возни с календарём и часовыми поясами): играть можно сколько
|
||
// угодно, просто сверх лимита выдача ресурса в этот день уже не
|
||
// происходит. Как только ресурс достигает своей цели, он
|
||
// "запирается в сейф" — суточный лимит на него больше не тратится
|
||
// (см. AwardResource), и игра остаётся доступной, а ресурс
|
||
// продолжает расти дальше без верхнего предела.
|
||
//
|
||
// Трупные батончики и Странник в сторимод не включены вовсе: первая
|
||
// — единственная игра коллекции без дискретной победы или
|
||
// поражения за одну партию, второй — открытый мир без единого
|
||
// чёткого условия "победы" за одну партию. Обе доступны как обычно
|
||
// в режиме "просто игры", но никак не участвуют в накоплении
|
||
// ресурсов сторимода.
|
||
|
||
// StoryResourceID — идентификатор одного ресурса сторимода,
|
||
// привязанного к конкретной игре коллекции.
|
||
type StoryResourceID int
|
||
|
||
const (
|
||
StoryResTonk StoryResourceID = iota
|
||
StoryResDurak
|
||
StoryResCheckers
|
||
StoryResNardy
|
||
StoryResBlackjack
|
||
StoryResChess
|
||
StoryResMinesweeper
|
||
StoryResGo
|
||
StoryResUr
|
||
StoryResSenet
|
||
StoryResOneOhOne
|
||
StoryResThousand
|
||
StoryResKlondike
|
||
StoryResTicTacToe
|
||
StoryResPoker
|
||
|
||
storyResourceCount // сколько всего ресурсов — не отдельный ресурс, а счётчик
|
||
)
|
||
|
||
// Цели по каждому ресурсу — отдельные именованные константы, а не
|
||
// одно общее число: у каждой игры со временем может появиться своя
|
||
// собственная цифра.
|
||
const (
|
||
StoryTargetTonk = 100
|
||
StoryTargetDurak = 100
|
||
StoryTargetCheckers = 100
|
||
StoryTargetNardy = 100
|
||
StoryTargetBlackjack = 100
|
||
StoryTargetChess = 100
|
||
StoryTargetMinesweeper = 100
|
||
StoryTargetGo = 100
|
||
StoryTargetUr = 100
|
||
StoryTargetSenet = 100
|
||
StoryTargetOneOhOne = 100
|
||
StoryTargetThousand = 100
|
||
StoryTargetKlondike = 100
|
||
StoryTargetTicTacToe = 10
|
||
StoryTargetPoker = 100
|
||
|
||
// storyStartingAmount — стартовое количество каждого ресурса в
|
||
// начале сторимода.
|
||
storyStartingAmount = 3
|
||
)
|
||
|
||
// storyResourceDef — статическое описание одного ресурса: ключ
|
||
// перевода названия и цель.
|
||
type storyResourceDef struct {
|
||
NameKey string
|
||
Target int
|
||
}
|
||
|
||
var storyResourceDefs = [storyResourceCount]storyResourceDef{
|
||
StoryResTonk: {NameKey: "story.resource.tonk", Target: StoryTargetTonk},
|
||
StoryResDurak: {NameKey: "story.resource.durak", Target: StoryTargetDurak},
|
||
StoryResCheckers: {NameKey: "story.resource.checkers", Target: StoryTargetCheckers},
|
||
StoryResNardy: {NameKey: "story.resource.nardy", Target: StoryTargetNardy},
|
||
StoryResBlackjack: {NameKey: "story.resource.blackjack", Target: StoryTargetBlackjack},
|
||
StoryResChess: {NameKey: "story.resource.chess", Target: StoryTargetChess},
|
||
StoryResMinesweeper: {NameKey: "story.resource.minesweeper", Target: StoryTargetMinesweeper},
|
||
StoryResGo: {NameKey: "story.resource.go", Target: StoryTargetGo},
|
||
StoryResUr: {NameKey: "story.resource.ur", Target: StoryTargetUr},
|
||
StoryResSenet: {NameKey: "story.resource.senet", Target: StoryTargetSenet},
|
||
StoryResOneOhOne: {NameKey: "story.resource.oneohone", Target: StoryTargetOneOhOne},
|
||
StoryResThousand: {NameKey: "story.resource.thousand", Target: StoryTargetThousand},
|
||
StoryResKlondike: {NameKey: "story.resource.klondike", Target: StoryTargetKlondike},
|
||
StoryResTicTacToe: {NameKey: "story.resource.tictactoe", Target: StoryTargetTicTacToe},
|
||
StoryResPoker: {NameKey: "story.resource.poker", Target: StoryTargetPoker},
|
||
}
|
||
|
||
// StoryDifficulty — уровень сложности, выбираемый один раз в начале
|
||
// сторимода и определяющий, какой уровень бота используется в
|
||
// каждой из игр, где такой выбор вообще есть.
|
||
type StoryDifficulty int
|
||
|
||
const (
|
||
StoryDifficultyEasy StoryDifficulty = iota
|
||
StoryDifficultyMedium
|
||
StoryDifficultyHard
|
||
)
|
||
|
||
// StoryOnboardingStage — этап знакомства перед тем, как открывается
|
||
// весь лагерь целиком со всеми 15 играми и видимыми числами
|
||
// ресурсов. Хранится отдельно от самих ресурсов (а не вычисляется
|
||
// из их текущих значений), потому что суточный расход мог бы
|
||
// откатить прогресс знакомства назад — переход всегда только вперёд.
|
||
type StoryOnboardingStage int
|
||
|
||
const (
|
||
// StoryOnboardingTicTacToeOnly — доступны только крестики-нолики,
|
||
// без чисел ресурсов и прочего антуража обычного лагеря.
|
||
StoryOnboardingTicTacToeOnly StoryOnboardingStage = iota
|
||
// StoryOnboardingWithCheckers — плюс шашки, всё ещё без чисел.
|
||
StoryOnboardingWithCheckers
|
||
// StoryOnboardingDone — обычный режим, весь лагерь как обычно.
|
||
StoryOnboardingDone
|
||
)
|
||
|
||
// StoryOnboardingTicTacToeWinsNeeded — сколько побед/ничьих в
|
||
// крестики-нолики нужно для открытия шашек.
|
||
const StoryOnboardingTicTacToeWinsNeeded = 3
|
||
|
||
// StoryOnboardingCheckersWinsNeeded — сколько побед в шашках нужно,
|
||
// чтобы знакомство завершилось и открылся весь лагерь.
|
||
const StoryOnboardingCheckersWinsNeeded = 1
|
||
|
||
// StoryDailyGrantsLimit — сколько раз в реальные сутки вообще может
|
||
// произойти начисление ресурса за победу (независимо от того,
|
||
// сколько партий реально сыграно) — обходной канал доступа к складу
|
||
// ограничен, чтобы не сжечь оставшиеся схемы.
|
||
const StoryDailyGrantsLimit = 3
|
||
|
||
// StoryStressFloor — стресс по умолчанию не опускается ниже этого
|
||
// значения.
|
||
const StoryStressFloor = 20
|
||
|
||
// StoryModeState — всё состояние партии сторимода.
|
||
type StoryModeState struct {
|
||
Difficulty StoryDifficulty
|
||
|
||
// Onboarding — этап знакомства (см. StoryOnboardingStage). Новая
|
||
// партия всегда начинается с StoryOnboardingTicTacToeOnly.
|
||
Onboarding StoryOnboardingStage
|
||
|
||
Resources [storyResourceCount]int
|
||
Locked [storyResourceCount]bool
|
||
|
||
// KlondikeWinsToday — сколько партий Косынки выиграно именно
|
||
// сегодня (сбрасывается каждые прожитые виртуальные сутки, см.
|
||
// Tick). У Косынки особое правило — 3 победы за один день, а не
|
||
// серия побед подряд или общий счётчик раундов.
|
||
KlondikeWinsToday int
|
||
|
||
// DailyGrantsUsed — сколько раз сегодня уже произошло начисление
|
||
// ресурса за победу (см. StoryDailyGrantsLimit и AwardResource).
|
||
DailyGrantsUsed int
|
||
|
||
// Стресс, отравление и содержание токсина в воздухе — проценты
|
||
// от 0 до 100.
|
||
Stress int
|
||
Poisoning int
|
||
ToxinLevel int
|
||
|
||
// FilterUsedToday — фильтр для воздуха израсходован именно
|
||
// сегодня (сбрасывается каждые новые сутки, см. Tick).
|
||
// DaysSinceFilterUsed — сколько суток подряд фильтр НЕ
|
||
// расходовался; чем больше, тем выше вероятность выброса
|
||
// нейротоксина за очередные сутки (см. storyNeurotoxinProbability).
|
||
// Сбрасывается в 0 только при реальном расходе фильтра, не при
|
||
// самом выбросе — если фильтр так и не поменяли, риск остаётся
|
||
// на максимуме и дальше.
|
||
FilterUsedToday bool
|
||
DaysSinceFilterUsed int
|
||
|
||
// DaysSinceDoorKnock — сколько суток подряд не было стука в
|
||
// дверь; чем больше, тем выше вероятность за очередные сутки (см.
|
||
// storyDoorKnockProbability). Сбрасывается в 0 при самом стуке.
|
||
DaysSinceDoorKnock int
|
||
|
||
// PendingNeurotoxinAlert/PendingDoorKnockAlert — хотя бы одно
|
||
// такое событие произошло с последнего показа соответствующего
|
||
// экрана-уведомления (см. stateStoryNeurotoxinAlert/
|
||
// stateStoryDoorKnockAlert) — показываются только при входе через
|
||
// меню режимов, по очереди, после приветствия дня.
|
||
PendingNeurotoxinAlert bool
|
||
PendingDoorKnockAlert bool
|
||
|
||
// Очистка воздуха, запущенная батарейкой: содержание токсина
|
||
// плавно снижается к нулю в течение часа с момента запуска.
|
||
AirPurificationActive bool
|
||
AirPurificationStart time.Time
|
||
AirPurificationFromLevel int
|
||
|
||
// Восстановление стресса после антидепрессанта: антидепрессант
|
||
// может опустить стресс ниже обычного пола (StoryStressFloor),
|
||
// вплоть до 0, но затем стресс плавно возвращается к полу в
|
||
// течение часа с момента приёма.
|
||
StressRecoveryActive bool
|
||
StressRecoveryStart time.Time
|
||
StressRecoveryFromLevel int
|
||
|
||
// LastTick — момент последнего учтённого перехода виртуальных
|
||
// суток (скользящие 24 часа, не календарные сутки) — используется
|
||
// для сброса суточных счётчиков (KlondikeWinsToday,
|
||
// DailyGrantsUsed, FilterUsedToday) и розыгрыша суточных событий
|
||
// (выброс нейротоксина, стук в дверь).
|
||
LastTick time.Time
|
||
}
|
||
|
||
// NewStoryModeState начинает новый сторимод с указанной сложностью:
|
||
// все ресурсы стартуют с storyStartingAmount, ничего не заперто,
|
||
// стресс 20%, отравление и токсин — 0%.
|
||
func NewStoryModeState(difficulty StoryDifficulty, now time.Time) *StoryModeState {
|
||
s := &StoryModeState{Difficulty: difficulty, LastTick: now, Stress: StoryStressFloor}
|
||
for i := range s.Resources {
|
||
s.Resources[i] = storyStartingAmount
|
||
}
|
||
return s
|
||
}
|
||
|
||
// Tick сбрасывает суточные счётчики (KlondikeWinsToday,
|
||
// DailyGrantsUsed) за каждые полные истёкшие "виртуальные сутки"
|
||
// (скользящие 24 часа с последнего учтённого тика). Возвращает,
|
||
// сколько таких суток было учтено (0, если ещё не прошли полные
|
||
// сутки) — вызывающий код использует это, чтобы понять, нужно ли
|
||
// показать утренний экран. Ресурсы сами по себе больше не
|
||
// расходуются со временем.
|
||
// Tick сбрасывает суточные счётчики (KlondikeWinsToday,
|
||
// DailyGrantsUsed, FilterUsedToday) и разыгрывает суточные события
|
||
// (выброс нейротоксина, стук в дверь — см. processDayEvents) за
|
||
// каждые полные истёкшие "виртуальные сутки" (скользящие 24 часа с
|
||
// последнего учтённого тика), каждый день по отдельности. Возвращает,
|
||
// сколько таких суток было учтено (0, если ещё не прошли полные
|
||
// сутки) — вызывающий код использует это, чтобы понять, нужно ли
|
||
// показать утренний экран.
|
||
func (s *StoryModeState) Tick(now time.Time) int {
|
||
elapsed := now.Sub(s.LastTick)
|
||
days := int(elapsed / (24 * time.Hour))
|
||
if days <= 0 {
|
||
return 0
|
||
}
|
||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
for i := 0; i < days; i++ {
|
||
s.processDayEvents(rnd)
|
||
}
|
||
s.KlondikeWinsToday = 0
|
||
s.DailyGrantsUsed = 0
|
||
s.LastTick = s.LastTick.Add(time.Duration(days) * 24 * time.Hour)
|
||
return days
|
||
}
|
||
|
||
// storyNeurotoxinProbability — вероятность выброса нейротоксина за
|
||
// сутки в зависимости от того, сколько суток подряд фильтр для
|
||
// воздуха не менялся. 0 суток (фильтр менялся сегодня) — выброса не
|
||
// может быть вовсе.
|
||
func storyNeurotoxinProbability(daysSinceFilter int) float64 {
|
||
switch {
|
||
case daysSinceFilter >= 3:
|
||
return 1.0
|
||
case daysSinceFilter == 2:
|
||
return 0.75
|
||
case daysSinceFilter >= 1:
|
||
return 0.5
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// storyDoorKnockProbability — вероятность стука в дверь за сутки в
|
||
// зависимости от того, сколько суток подряд его не было. Ровно на
|
||
// третьи сутки (и далее) — гарантированно.
|
||
func storyDoorKnockProbability(daysSinceKnock int) float64 {
|
||
switch {
|
||
case daysSinceKnock >= 3:
|
||
return 1.0
|
||
case daysSinceKnock == 2:
|
||
return 0.5
|
||
case daysSinceKnock >= 1:
|
||
return 0.1
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// processDayEvents разыгрывает события ровно одних суток: выброс
|
||
// нейротоксина (зависит от того, менялся ли фильтр в эти сутки) и
|
||
// стук в дверь (не зависит ни от чего, кроме того, сколько суток его
|
||
// не было). Может произойти и то, и другое в один и тот же день.
|
||
func (s *StoryModeState) processDayEvents(rnd *rand.Rand) {
|
||
if s.FilterUsedToday {
|
||
s.DaysSinceFilterUsed = 0
|
||
} else {
|
||
s.DaysSinceFilterUsed++
|
||
}
|
||
if rnd.Float64() < storyNeurotoxinProbability(s.DaysSinceFilterUsed) {
|
||
power := 10 + rnd.Intn(61) // 10..70 включительно
|
||
s.ToxinLevel += power
|
||
if s.ToxinLevel > 100 {
|
||
s.ToxinLevel = 100
|
||
}
|
||
s.PendingNeurotoxinAlert = true
|
||
}
|
||
s.FilterUsedToday = false
|
||
|
||
s.DaysSinceDoorKnock++
|
||
if rnd.Float64() < storyDoorKnockProbability(s.DaysSinceDoorKnock) {
|
||
s.Stress += 20
|
||
if s.Stress > 100 {
|
||
s.Stress = 100
|
||
}
|
||
s.PendingDoorKnockAlert = true
|
||
s.DaysSinceDoorKnock = 0
|
||
}
|
||
}
|
||
|
||
// RecordWin начисляет +1 ресурса id безусловно, без учёта суточного
|
||
// лимита — используется только изнутри AwardResource. Если это
|
||
// начисление как раз доводит ресурс до цели, он тут же запирается в
|
||
// сейф, что не блокирует ни саму игру, ни дальнейшее накопление:
|
||
// после фиксации игра остаётся доступной, и ресурс просто продолжает
|
||
// расти дальше без верхнего предела.
|
||
func (s *StoryModeState) RecordWin(id StoryResourceID) {
|
||
s.Resources[id]++
|
||
if !s.Locked[id] && s.Resources[id] >= storyResourceDefs[id].Target {
|
||
s.Locked[id] = true
|
||
}
|
||
}
|
||
|
||
// AwardResource пытается начислить +1 ресурса id за победу, но
|
||
// только если дневной лимит начислений (StoryDailyGrantsLimit) ещё
|
||
// не исчерпан за сегодня. Возвращает true, если начисление
|
||
// действительно произошло. У Косынки помимо этого лимита есть ещё
|
||
// собственное правило "3 победы за один день" — вызывающий код
|
||
// (menu.go) отслеживает это отдельно через KlondikeWinsToday, прежде
|
||
// чем вызвать AwardResource.
|
||
func (s *StoryModeState) AwardResource(id StoryResourceID) bool {
|
||
if s.DailyGrantsUsed >= StoryDailyGrantsLimit {
|
||
return false
|
||
}
|
||
s.RecordWin(id)
|
||
s.DailyGrantsUsed++
|
||
return true
|
||
}
|
||
|
||
// spendResource списывает amount единиц ресурса id, если их
|
||
// достаточно. Если после списания количество опускается ниже цели,
|
||
// ресурс перестаёт считаться запертым в сейфе — трата действительно
|
||
// расходует накопленное, а не просто "тратит излишек".
|
||
func (s *StoryModeState) spendResource(id StoryResourceID, amount int) bool {
|
||
if s.Resources[id] < amount {
|
||
return false
|
||
}
|
||
s.Resources[id] -= amount
|
||
if s.Resources[id] < storyResourceDefs[id].Target {
|
||
s.Locked[id] = false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// SpendAntidepressant тратит 1 антидепрессант (ресурс Косынки) и
|
||
// снижает стресс на 20%, не опускаясь ниже StoryStressFloor.
|
||
// SpendAntidepressant тратит 1 антидепрессант (ресурс Косынки) и
|
||
// снижает стресс на 20%, на этот раз не ограничиваясь обычным полом
|
||
// StoryStressFloor — стресс может опуститься вплоть до 0%. Если
|
||
// после этого стресс оказался ниже обычного пола, запускается
|
||
// часовое восстановление обратно к полу (см. ApplyStressRecovery).
|
||
func (s *StoryModeState) SpendAntidepressant(now time.Time) bool {
|
||
if !s.spendResource(StoryResKlondike, 1) {
|
||
return false
|
||
}
|
||
// сначала актуализируем уже идущее восстановление (если оно
|
||
// есть), чтобы вычитать 20 из настоящего текущего значения, а не
|
||
// из устаревшего
|
||
s.ApplyStressRecovery(now)
|
||
s.Stress -= 20
|
||
if s.Stress < 0 {
|
||
s.Stress = 0
|
||
}
|
||
if s.Stress < StoryStressFloor {
|
||
s.StressRecoveryActive = true
|
||
s.StressRecoveryStart = now
|
||
s.StressRecoveryFromLevel = s.Stress
|
||
} else {
|
||
s.StressRecoveryActive = false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ApplyStressRecovery проверяет, идёт ли сейчас восстановление
|
||
// стресса после антидепрессанта, и если да — плавно поднимает
|
||
// стресс от StressRecoveryFromLevel к StoryStressFloor, пропорционально
|
||
// прошедшему времени, вплоть до полного завершения через час.
|
||
func (s *StoryModeState) ApplyStressRecovery(now time.Time) {
|
||
if !s.StressRecoveryActive {
|
||
return
|
||
}
|
||
elapsed := now.Sub(s.StressRecoveryStart)
|
||
if elapsed >= time.Hour {
|
||
s.Stress = StoryStressFloor
|
||
s.StressRecoveryActive = false
|
||
return
|
||
}
|
||
frac := float64(elapsed) / float64(time.Hour)
|
||
s.Stress = s.StressRecoveryFromLevel + int(float64(StoryStressFloor-s.StressRecoveryFromLevel)*frac)
|
||
if s.Stress > StoryStressFloor {
|
||
s.Stress = StoryStressFloor
|
||
}
|
||
}
|
||
|
||
// SpendAntidote тратит 1 антидот (ресурс Сапёра) и снижает
|
||
// отравление на 20%, не опускаясь ниже нуля.
|
||
func (s *StoryModeState) SpendAntidote() bool {
|
||
if !s.spendResource(StoryResMinesweeper, 1) {
|
||
return false
|
||
}
|
||
s.Poisoning -= 20
|
||
if s.Poisoning < 0 {
|
||
s.Poisoning = 0
|
||
}
|
||
return true
|
||
}
|
||
|
||
// SpendAirFilter тратит 1 фильтр для воздуха (ресурс Го) — отмечает
|
||
// фильтр использованным сегодня. При обработке перехода суток (см.
|
||
// Tick) это защитит от выброса нейротоксина за сегодняшний день и
|
||
// сбросит счётчик дней без замены (DaysSinceFilterUsed) в 0.
|
||
func (s *StoryModeState) SpendAirFilter() bool {
|
||
if !s.spendResource(StoryResGo, 1) {
|
||
return false
|
||
}
|
||
s.FilterUsedToday = true
|
||
return true
|
||
}
|
||
|
||
// SpendBattery тратит 1 батарейку (ресурс 101) и запускает очистку
|
||
// воздуха: содержание токсина плавно снижается к нулю в течение часа
|
||
// с этого момента (см. ApplyAirPurification).
|
||
func (s *StoryModeState) SpendBattery(now time.Time) bool {
|
||
if !s.spendResource(StoryResOneOhOne, 1) {
|
||
return false
|
||
}
|
||
s.AirPurificationActive = true
|
||
s.AirPurificationStart = now
|
||
s.AirPurificationFromLevel = s.ToxinLevel
|
||
return true
|
||
}
|
||
|
||
// ApplyAirPurification проверяет, идёт ли сейчас запущенная батарейкой
|
||
// очистка воздуха, и если да — снижает содержание токсина
|
||
// пропорционально прошедшему времени, вплоть до полного завершения
|
||
// (токсин = 0) через час с момента запуска.
|
||
func (s *StoryModeState) ApplyAirPurification(now time.Time) {
|
||
if !s.AirPurificationActive {
|
||
return
|
||
}
|
||
elapsed := now.Sub(s.AirPurificationStart)
|
||
if elapsed >= time.Hour {
|
||
s.ToxinLevel = 0
|
||
s.AirPurificationActive = false
|
||
return
|
||
}
|
||
frac := float64(elapsed) / float64(time.Hour)
|
||
s.ToxinLevel = s.AirPurificationFromLevel - int(float64(s.AirPurificationFromLevel)*frac)
|
||
if s.ToxinLevel < 0 {
|
||
s.ToxinLevel = 0
|
||
}
|
||
}
|
||
|
||
// AdvanceOnboarding проверяет, не пора ли перейти на следующий этап
|
||
// знакомства, исходя из текущих значений ресурсов крестиков-ноликов
|
||
// и шашек. Переход всегда только вперёд (см. StoryOnboardingStage) —
|
||
// вызывать безопасно после любой партии, не только тех двух игр.
|
||
// Возвращает true, если именно этим вызовом знакомство полностью
|
||
// завершилось (переход в StoryOnboardingDone) — по этому сигналу
|
||
// вызывающий код показывает приветствие в первый и единственный раз.
|
||
func (s *StoryModeState) AdvanceOnboarding() (justCompleted bool) {
|
||
switch s.Onboarding {
|
||
case StoryOnboardingTicTacToeOnly:
|
||
// ресурсы стартуют не с нуля, а с storyStartingAmount — порог
|
||
// считаем от стартового значения, иначе условие "3 победы"
|
||
// выполнилось бы сразу, без единой реально сыгранной партии
|
||
if s.Resources[StoryResTicTacToe] >= storyStartingAmount+StoryOnboardingTicTacToeWinsNeeded {
|
||
s.Onboarding = StoryOnboardingWithCheckers
|
||
}
|
||
case StoryOnboardingWithCheckers:
|
||
if s.Resources[StoryResCheckers] >= storyStartingAmount+StoryOnboardingCheckersWinsNeeded {
|
||
s.Onboarding = StoryOnboardingDone
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// AllResourcesLocked проверяет, заперты ли уже все ресурсы сторимода
|
||
// (все игры, кроме Странника и Трупных батончиков, которые в
|
||
// сторимоде не участвуют вовсе).
|
||
func (s *StoryModeState) AllResourcesLocked() bool {
|
||
for _, locked := range s.Locked {
|
||
if !locked {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Complete проверяет полное завершение сторимода: заперты все
|
||
// ресурсы.
|
||
func (s *StoryModeState) Complete() bool {
|
||
return s.AllResourcesLocked()
|
||
}
|