Files

100 lines
4.8 KiB
C#
Raw Permalink Normal View History

using System;
using System.Collections.Generic;
using System.Globalization;
// РЕДКИЙ PvE-ТОРГОВЕЦ: синий кристалл за золото (Регламент валют, кристаллов и торговли от 05.11.2026, разд. 3, 10, 12;
// «Ответ Володе» от 06.11.2026, п. 2). Документ: Docs/Modules/Economy.md, раздел «Синие кристаллы».
//
// - За скользящие 7 реальных суток: 1-я покупка -- Prices[0] (ТЕСТ 30 000 золота), 2-я -- Prices[1] (60 000),
// дальше -- недоступно (не больше Prices.Length покупок).
// - Покупка входит в недельный PvE-предел (CrystalQuota). Нет квоты -- покупка заблокирована, золото НЕ списывается,
// компенсации за оплаченный товар нет.
// - Нет места в хранилище -- тоже блок без списания (вместимость получателя проверяется до оплаты).
// - Порядок: все проверки -> списание золота (всё или ничего) -> квота -> запись покупки. Одна операция -- один id,
// повтор того же id ничего не делает.
// Без Unity: те же правила будут на сервере и в тестах.
public sealed class CrystalTrader
{
public enum Result { Bought, NoSlots, NoQuota, NoSpace, NoGold, Repeated }
public readonly int[] Prices;
public readonly long WindowMs;
struct Purchase { public long TimeMs; public string Id; }
readonly List<Purchase> purchases = new List<Purchase>();
public CrystalTrader(int[] prices = null, long windowMs = 7 * CrystalQuota.Day)
{
Prices = prices != null && prices.Length > 0 ? prices : new[] { 30000, 60000 };
WindowMs = Math.Max(1, windowMs);
}
public int BoughtInWindow(long nowMs)
{
int n = 0;
foreach (var p in purchases) if (p.TimeMs > nowMs - WindowMs && p.TimeMs <= nowMs) n++;
return n;
}
// Цена следующей покупки; -1 -- в этом окне больше нельзя
public int PriceNow(long nowMs)
{
int n = BoughtInWindow(nowMs);
return n < Prices.Length ? Prices[n] : -1;
}
// Когда снова можно купить (мс), если покупки кончились; 0 -- можно сейчас
public long NextSlotMs(long nowMs)
{
if (PriceNow(nowMs) >= 0) return 0;
long first = long.MaxValue;
foreach (var p in purchases) if (p.TimeMs > nowMs - WindowMs && p.TimeMs < first) first = p.TimeMs;
return first == long.MaxValue ? 0 : first + WindowMs;
}
// Что помешает купить сейчас (без списания); Bought -- можно
public Result Check(long nowMs, CrystalQuota quota, int freeSpace)
{
if (PriceNow(nowMs) < 0) return Result.NoSlots;
if (quota != null && !quota.CanTakeAll(1, nowMs)) return Result.NoQuota;
if (freeSpace < 1) return Result.NoSpace;
return Result.Bought;
}
// Купить 1 синий кристалл. trySpendGold(цена) -- списание «всё или ничего». При Bought кристалл начисляет вызывающий
public Result TryBuy(string id, long nowMs, CrystalQuota quota, int freeSpace, Func<int, bool> trySpendGold)
{
if (!string.IsNullOrEmpty(id) && purchases.Exists(p => p.Id == id)) return Result.Repeated;
var check = Check(nowMs, quota, freeSpace);
if (check != Result.Bought) return check;
int price = PriceNow(nowMs);
if (trySpendGold == null || !trySpendGold(price)) return Result.NoGold;
if (quota != null) quota.Take(id, 1, nowMs);
purchases.RemoveAll(p => p.TimeMs <= nowMs - WindowMs);
purchases.Add(new Purchase { TimeMs = nowMs, Id = id ?? "" });
return Result.Bought;
}
// ---------------- Сохранение: строки "время|id" ----------------
public List<string> ToLines()
{
var lines = new List<string>();
foreach (var p in purchases) lines.Add(p.TimeMs.ToString(CultureInfo.InvariantCulture) + "|" + p.Id);
return lines;
}
public void Load(IEnumerable<string> lines)
{
purchases.Clear();
if (lines == null) return;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line)) continue;
var p = line.Split(new[] { '|' }, 2);
if (!long.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long t)) continue;
purchases.Add(new Purchase { TimeMs = t, Id = p.Length > 1 ? p[1] : "" });
}
}
}