Files
MVP1/Assets/Scripts/Modules/Economy/Rules/CrystalQuota.cs
T
AlexanderandClaude Opus 5.5 277afdf84e Недельный PvE-предел синих кристаллов: 6 за 7 реальных суток, сверх -- золото (регламент валют 05.11, ответ 06.11)
- CrystalQuota (без Unity) + 6 тестов из раздела 12
- Задания, события, бои: в пределах квоты -- кристаллы, сверх -- 1000 золота за кристалл (ТЕСТ)
- Ждущие места кристаллы квоту второй раз не тратят; письмо «ждут места» -- вне квоты
- Окно Ратуши: «Новых кристаллов за 7 дней: N / 6»; DEV: «+1 PvE-кристалл», «Сбросить квоту»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:24:16 +01:00

95 lines
5.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
// НЕДЕЛЬНЫЙ PvE-ПРЕДЕЛ СИНИХ КРИСТАЛЛОВ (Регламент валют, кристаллов и торговли от 05.11.2026, разд. 5.2, 10, 12;
// «Ответ Володе» от 06.11.2026, п. 2). Документ: Docs/Modules/Economy.md, раздел «Синие кристаллы».
//
// - Не больше Limit (ТЕСТ 6) НОВЫХ синих кристаллов за скользящие WindowMs (7 реальных суток) из PvE: задания,
// события, бои, PvE-торговец. Кристальная шахта, торговля и грабёж -- вне предела.
// - Take: сколько из награды можно выдать кристаллами сейчас; остальное вызывающий заменяет компенсацией
// (золото / ресурсы). На следующую неделю излишек не переносится.
// - Квота и место в хранилище -- разные условия: взятое по квоте считается сразу, даже если кристаллы потом
// ждут места, -- второй раз квота за них не тратится.
// - Операция с тем же id второй раз квоту не берёт (повторное «Забрать», загрузка).
// - Время -- метки (мс), окно скользящее: запись старше окна больше не считается. На сервере -- доверенное время.
// Без Unity: те же правила будут на сервере и в тестах.
public sealed class CrystalQuota
{
public const long Day = 24L * 60 * 60 * 1000;
public readonly int Limit;
public readonly long WindowMs;
struct Entry { public long TimeMs; public int Amount; public string Id; }
readonly List<Entry> entries = new List<Entry>();
public CrystalQuota(int limit = 6, long windowMs = 7 * Day)
{
Limit = Math.Max(0, limit);
WindowMs = Math.Max(1, windowMs);
}
// Сколько новых PvE-кристаллов уже выдано за окно, кончающееся в nowMs
public int Used(long nowMs)
{
int n = 0;
foreach (var e in entries) if (e.TimeMs > nowMs - WindowMs && e.TimeMs <= nowMs) n += e.Amount;
return n;
}
public int Left(long nowMs) => Math.Max(0, Limit - Used(nowMs));
// Когда освободится хотя бы один кристалл квоты (мс); 0 -- уже свободно
public long NextFreeMs(long nowMs)
{
if (Left(nowMs) > 0) return 0;
long first = long.MaxValue;
foreach (var e in entries) if (e.TimeMs > nowMs - WindowMs && e.TimeMs < first) first = e.TimeMs;
return first == long.MaxValue ? 0 : first + WindowMs;
}
public bool Taken(string id) => !string.IsNullOrEmpty(id) && entries.Exists(e => e.Id == id);
// Взять квоту под награду: сколько из amount выдать кристаллами (остальное -- компенсацией).
// Тот же id второй раз -- 0 (уже учтено). Записи старше окна убираются
public int Take(string id, int amount, long nowMs)
{
Prune(nowMs);
if (amount <= 0 || Taken(id)) return 0;
int give = Math.Min(amount, Left(nowMs));
if (give > 0 || !string.IsNullOrEmpty(id)) entries.Add(new Entry { TimeMs = nowMs, Amount = give, Id = id ?? "" });
return give;
}
// Можно ли взять amount целиком (покупка у PvE-торговца: при нехватке -- блок, золото не списывается)
public bool CanTakeAll(int amount, long nowMs) => amount <= Left(nowMs);
void Prune(long nowMs) => entries.RemoveAll(e => e.TimeMs <= nowMs - WindowMs);
// ---------------- Сохранение: строки "время|сколько|id" ----------------
public List<string> ToLines()
{
var lines = new List<string>();
foreach (var e in entries)
lines.Add(e.TimeMs.ToString(CultureInfo.InvariantCulture) + "|" + e.Amount.ToString(CultureInfo.InvariantCulture) + "|" + e.Id);
return lines;
}
public void Load(IEnumerable<string> lines)
{
entries.Clear();
if (lines == null) return;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line)) continue;
var p = line.Split(new[] { '|' }, 3);
if (p.Length < 2) continue;
if (!long.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long t)) continue;
if (!int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) || n < 0) continue;
entries.Add(new Entry { TimeMs = t, Amount = n, Id = p.Length > 2 ? p[2] : "" });
}
}
}