using System; using System.Collections.Generic; using System.Globalization; // КРИСТАЛЬНАЯ ШАХТА: выработка синих кристаллов (Регламент редкости синих кристаллов от 05.11.2026, разд. 2.1; // Регламент валют от 05.11, разд. 5.2 и 8.1; «Ответ Володе» от 06.11, п. 3). Документ: Docs/Modules/Economy.md. // // - Каждая работающая шахта -- PerPeriod (ТЕСТ 3) кристаллов за PeriodMs (7 РЕАЛЬНЫХ суток; не игровые часы ×12), // равномерно: один кристалл за PeriodMs / PerPeriod. Шахты (до двух) складываются. // - Шахта работает с момента, когда достроена (activeFromMs): стройка, закончившаяся офлайн, добывает с её конца. // - Хранилище полное -- пауза: прогресс не растёт, скрытого запаса нет. Хранилище заполнилось в ходе расчёта -- // шахта встаёт сразу после последнего поместившегося кристалла (прогресс 0). Освободилось место -- выработка идёт // дальше с оставшегося прогресса, без начисления за время паузы. // - Офлайн: досчёт по меткам при входе; уже посчитанное время второй раз не начисляется; часы назад (сброс // DEV-перемотки) -- метка не уходит назад, ничего не начисляется. // - В недельный PvE-предел не входит (отдельный учёт). // Без Unity: те же правила будут на сервере и в тестах. public sealed class CrystalMine { public readonly int PerPeriod; public readonly long PeriodMs; // Прогресс к следующему кристаллу (мс работы одной шахты) и до какого момента посчитано public long ProgressMs { get; private set; } public long LastMs { get; private set; } public CrystalMine(int perPeriod = 3, long periodMs = 7 * CrystalQuota.Day) { PerPeriod = Math.Max(1, perPeriod); PeriodMs = Math.Max(1, periodMs); } public long MsPerCrystal => Math.Max(1, PeriodMs / PerPeriod); // Досчитать до nowMs. mineActiveFromMs -- по шахте: с какого момента она работает (long.MinValue -- давно). // freeSpace -- сколько кристаллов помещается в хранилище. Возвращает, сколько кристаллов начислить public int Advance(long nowMs, IEnumerable mineActiveFromMs, int freeSpace) { if (LastMs == 0) { LastMs = nowMs; return 0; } // первый расчёт: отсюда и считаем if (nowMs <= LastMs) return 0; // часы назад или то же время -- ничего long from = LastMs; LastMs = nowMs; if (freeSpace <= 0) return 0; // пауза: хранилище полное, прогресс не растёт long work = 0; if (mineActiveFromMs != null) foreach (long active in mineActiveFromMs) { long start = Math.Max(from, active); if (start < nowMs) work += nowMs - start; } if (work <= 0) return 0; long total = ProgressMs + work; long made = total / MsPerCrystal; if (made <= freeSpace) { ProgressMs = total - made * MsPerCrystal; return (int)made; } ProgressMs = 0; // хранилище заполнилось: шахта встала сразу после последнего кристалла, лишнее не копится return freeSpace; } // Через сколько мс следующий кристалл при count работающих шахтах (0 шахт -- -1) public long MsToNext(int count) => count <= 0 ? -1 : Math.Max(0, (MsPerCrystal - ProgressMs + count - 1) / count); public void Load(string line) { ProgressMs = 0; LastMs = 0; if (string.IsNullOrEmpty(line)) return; var p = line.Split('|'); if (p.Length >= 2 && long.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long progress) && long.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out long last)) { ProgressMs = Math.Max(0, Math.Min(progress, MsPerCrystal - 1)); LastMs = Math.Max(0, last); } } public string ToLine() => ProgressMs.ToString(CultureInfo.InvariantCulture) + "|" + LastMs.ToString(CultureInfo.InvariantCulture); }