- CrystalTrader (без Unity) + 5 тестов из раздела 12 - Входит в недельный PvE-предел; нет квоты, места или покупок -- блок, золото не списывается - Магазин: карточка «Редкий торговец»; DEV «Золото 100 000» для проверки Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
4.8 KiB
C#
100 lines
4.8 KiB
C#
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] : "" });
|
|
}
|
|
}
|
|
}
|