Feature/blue crystals #19
@@ -80,6 +80,8 @@ public class DebugPanel : MonoBehaviour
|
||||
// Недельный PvE-предел кристаллов: «+1 PvE-кристалл» выдаёт как настоящая награда (квота, место, компенсация)
|
||||
var rowQ = UIKit.Row(card, 70, 10);
|
||||
Btn(rowQ, "+1 PvE-кристалл", UIKit.Theme.Blue, () => { if (GameProgress.Instance != null) GameProgress.Instance.Grant(new[] { new RewardItem(ResourceType.Gems, 1) }); Refresh(); });
|
||||
// Только для проверки торговца (30 000 / 60 000): золото ставится напрямую, мимо лимита склада
|
||||
Btn(rowQ, "Золото 100 000", UIKit.Theme.Green, () => { if (ResourceManager.Instance != null) ResourceManager.Instance.SetResource(ResourceType.Gold, 100000); Refresh(); });
|
||||
Btn(rowQ, "Сбросить квоту", UIKit.Theme.Gray, () => { if (GameProgress.Instance != null) GameProgress.Instance.DevResetCrystalQuota(); UIKit.Toast("Недельная квота кристаллов сброшена"); Refresh(); });
|
||||
|
||||
var row3 = UIKit.Row(card, 70, 10);
|
||||
|
||||
@@ -105,6 +105,9 @@ public class EconomyConfig : ScriptableObject
|
||||
public int crystalPveWindowDays = 7;
|
||||
[Tooltip("Компенсация золотом за каждый синий кристалл PvE-награды сверх недельного предела (величина не согласована). ТЕСТ")]
|
||||
public float crystalCompensationGold = 1000f;
|
||||
[Tooltip("Редкий PvE-торговец: цена синего кристалла в золоте по порядку покупок за 7 реальных суток; покупок не больше, " +
|
||||
"чем цен (регламент валют 05.11, разд. 3). Входит в недельный PvE-предел. ТЕСТ")]
|
||||
public int[] crystalTraderPrices = { 30000, 60000 };
|
||||
[Tooltip("Платные расширения хранилища кристаллов по порядку: с какой Ратуши, до скольки, цена (золото и обычные ресурсы). " +
|
||||
"Ратуша только разрешает -- само не растёт (регламент валют 05.11, разд. 8; ответ 06.11, п. 1). Числа и цены ТЕСТ")]
|
||||
public CrystalExpansion[] crystalExpansions =
|
||||
|
||||
@@ -150,6 +150,10 @@ public class GameProgress : MonoBehaviour
|
||||
var storageCfg = EconomyConfig.Load().storage;
|
||||
quota = new CrystalQuota(storageCfg.crystalPveLimit, Math.Max(1, storageCfg.crystalPveWindowDays) * CrystalQuota.Day);
|
||||
quota.Load(Data.crystalQuota);
|
||||
if (Data.crystalTrader == null) Data.crystalTrader = new List<string>();
|
||||
if (Data.crystalTraderNextId < 1) Data.crystalTraderNextId = 1;
|
||||
trader = new CrystalTrader(storageCfg.crystalTraderPrices, Math.Max(1, storageCfg.crystalPveWindowDays) * CrystalQuota.Day);
|
||||
trader.Load(Data.crystalTrader);
|
||||
if (Data.mail == null) Data.mail = new List<MailItem>();
|
||||
if (Data.shopIds == null) Data.shopIds = new List<string>();
|
||||
if (Data.shopCounts == null) Data.shopCounts = new List<int>();
|
||||
@@ -377,11 +381,62 @@ public class GameProgress : MonoBehaviour
|
||||
return allowed;
|
||||
}
|
||||
|
||||
// ---------------- Редкий PvE-торговец (правила -- CrystalTrader) ----------------
|
||||
|
||||
CrystalTrader trader = new CrystalTrader();
|
||||
|
||||
public int TraderPrice => trader.PriceNow(GameClock.UnixNowMs); // -1 -- в этом окне больше нельзя
|
||||
public int TraderBought => trader.BoughtInWindow(GameClock.UnixNowMs);
|
||||
public int TraderMaxPurchases => trader.Prices.Length;
|
||||
public double TraderSecondsToSlot
|
||||
{
|
||||
get { long at = trader.NextSlotMs(GameClock.UnixNowMs); return at <= 0 ? 0 : Math.Max(0, at - GameClock.UnixNowMs) / 1000.0; }
|
||||
}
|
||||
|
||||
// Почему сейчас нельзя купить (без списания); null -- можно, если хватит золота
|
||||
public string TraderBlockReason()
|
||||
{
|
||||
switch (trader.Check(GameClock.UnixNowMs, quota, CrystalRewards.Free(GemsHave, GemsCap)))
|
||||
{
|
||||
case CrystalTrader.Result.NoSlots: return $"Куплено {TraderBought} из {TraderMaxPurchases} за 7 дней. Следующая покупка -- через {GameData.FormatTime(TraderSecondsToSlot)}";
|
||||
case CrystalTrader.Result.NoQuota: return $"Недельный предел новых кристаллов исчерпан ({quota.Limit} за 7 дней). Освободится через {GameData.FormatTime(CrystalQuotaSecondsToFree)}";
|
||||
case CrystalTrader.Result.NoSpace: return $"Хранилище кристаллов заполнено ({GemsHave:0} / {GemsCap:0}). Расширить -- в окне Ратуши";
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Купить 1 синий кристалл. Проверки (покупки, квота, место) -- до оплаты: при отказе золото не списывается
|
||||
public void BuyCrystalFromTrader()
|
||||
{
|
||||
string id = "trader_" + Data.crystalTraderNextId;
|
||||
var result = trader.TryBuy(id, GameClock.UnixNowMs, quota, CrystalRewards.Free(GemsHave, GemsCap),
|
||||
price => GameServices.TryGet(out IEconomy e) && e.TrySpend(ResourceType.Gold, price));
|
||||
if (result != CrystalTrader.Result.Bought)
|
||||
{
|
||||
UIKit.Toast(result == CrystalTrader.Result.NoGold ? $"Нужно {TraderPrice:N0} золота" : (TraderBlockReason() ?? "Покупка недоступна"));
|
||||
return;
|
||||
}
|
||||
Data.crystalTraderNextId++;
|
||||
if (ResourceManager.Instance != null)
|
||||
{
|
||||
suppressTracking++;
|
||||
ResourceManager.Instance.AddResource(ResourceType.Gems, 1); // место проверено до оплаты
|
||||
suppressTracking--;
|
||||
}
|
||||
Data.crystalTrader = trader.ToLines();
|
||||
Data.crystalQuota = quota.ToLines();
|
||||
MarkChanged();
|
||||
if (SaveManager.Instance != null) SaveManager.Instance.SaveGame(); // золото, кристалл и покупка -- сразу на диск
|
||||
UIKit.Toast("Куплен 1 синий кристалл");
|
||||
}
|
||||
|
||||
// Только для проверки (DEV): забыть недельную квоту
|
||||
public void DevResetCrystalQuota()
|
||||
{
|
||||
quota.Load(null);
|
||||
trader.Load(null);
|
||||
Data.crystalQuota = quota.ToLines();
|
||||
Data.crystalTrader = trader.ToLines();
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ public class ProgressData
|
||||
public List<string> crystalWaiting = new List<string>();
|
||||
// Недельный PvE-предел синих кристаллов (регламент валют 05.11): "время мс|сколько|id операции"
|
||||
public List<string> crystalQuota = new List<string>();
|
||||
// Покупки у редкого PvE-торговца (регламент валют 05.11, разд. 3): "время мс|id покупки"
|
||||
public List<string> crystalTrader = new List<string>();
|
||||
public int crystalTraderNextId = 1;
|
||||
public float[] dailyCollected = new float[8];
|
||||
public int dailyBattles;
|
||||
public int dailyTroops;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// МАГАЗИН: бесплатный сундук раз в день, пакеты ресурсов за кристаллы, обмен золота,
|
||||
// витрина платных наборов (пока "скоро"). Лимиты покупок сбрасываются в полночь.
|
||||
// МАГАЗИН: редкий PvE-торговец (синий кристалл за золото), бесплатный сундук раз в день, пакеты ресурсов за золото.
|
||||
// Лимиты пакетов сбрасываются в полночь; у торговца -- свои: не больше 2 кристаллов за 7 реальных суток.
|
||||
public class ShopPanel : BasePanel
|
||||
{
|
||||
TextMeshProUGUI timer;
|
||||
@@ -15,6 +15,8 @@ public class ShopPanel : BasePanel
|
||||
WalletItem(wallet, ResourceType.Gems);
|
||||
WalletItem(wallet, ResourceType.Gold);
|
||||
|
||||
TraderCard(list);
|
||||
|
||||
timer = UIKit.Text(list, "", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 40);
|
||||
Tick();
|
||||
|
||||
@@ -52,6 +54,30 @@ public class ShopPanel : BasePanel
|
||||
}
|
||||
}
|
||||
|
||||
// Редкий PvE-торговец (регламент валют 05.11, разд. 3): дорогой обмен золота на синий кристалл
|
||||
void TraderCard(Transform list)
|
||||
{
|
||||
var gp = GameProgress.Instance;
|
||||
int price = gp.TraderPrice;
|
||||
string block = gp.TraderBlockReason();
|
||||
bool enough = price >= 0 && gp.Res(ResourceType.Gold) >= price;
|
||||
|
||||
var card = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
var row = UIKit.Row(card, 150, 16);
|
||||
UIKit.Icon(row, GameData.ResColor(ResourceType.Gems), "", 110);
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 4);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
var title = UIKit.Text(info, "Редкий торговец: синий кристалл", 30);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, $"Куплено за 7 дней: {gp.TraderBought} / {gp.TraderMaxPurchases}. Входит в недельный предел новых кристаллов " +
|
||||
$"({gp.CrystalQuotaUsed} / {gp.CrystalQuotaLimit})", 22, UIKit.Theme.TextDim);
|
||||
if (block != null) UIKit.Text(info, block, 22, UIKit.Theme.Gold);
|
||||
|
||||
string label = price < 0 ? "Недоступно" : price.ToString("N0") + " золота";
|
||||
var b = UIKit.Button(row, label, block == null && enough ? UIKit.Theme.Blue : UIKit.Theme.Gray, () => { gp.BuyCrystalFromTrader(); RequestRebuild(); }, 260, 100, 24);
|
||||
b.interactable = block == null;
|
||||
}
|
||||
|
||||
static void WalletItem(Transform parent, ResourceType t)
|
||||
{
|
||||
UIKit.Icon(parent, GameData.ResColor(t), "", 40);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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] : "" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c25a8e3f7d1b4c69b0e4a6f9d8c37b21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -584,3 +584,87 @@ public class CrystalQuotaTests
|
||||
Assert.AreEqual(2, q.Used(T0 + 10), "квота потрачена один раз: 2, а не 4");
|
||||
}
|
||||
}
|
||||
|
||||
// Редкий PvE-торговец: синий кристалл за золото (регламент валют 05.11, разд. 3, 10, 12)
|
||||
public class CrystalTraderTests
|
||||
{
|
||||
const long T0 = 1_800_000_000_000L;
|
||||
const long Day = CrystalQuota.Day;
|
||||
|
||||
static System.Func<int, bool> Wallet(float[] gold) => price =>
|
||||
{
|
||||
if (gold[0] < price) return false;
|
||||
gold[0] -= price;
|
||||
return true;
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Trader_Quota4of6_Buys_ChargesGold_CountsQuota_Max2()
|
||||
{
|
||||
var quota = new CrystalQuota(6, 7 * Day);
|
||||
quota.Take("old", 4, T0);
|
||||
var t = new CrystalTrader(new[] { 30000, 60000 }, 7 * Day);
|
||||
var gold = new[] { 100000f };
|
||||
Assert.AreEqual(CrystalTrader.Result.Bought, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
||||
Assert.AreEqual(70000f, gold[0], 0.01, "первая -- 30 000");
|
||||
Assert.AreEqual(5, quota.Used(T0 + 1), "квота +1");
|
||||
Assert.AreEqual(CrystalTrader.Result.Bought, t.TryBuy("b2", T0 + 2, quota, 10, Wallet(gold)));
|
||||
Assert.AreEqual(10000f, gold[0], 0.01, "вторая -- 60 000");
|
||||
Assert.AreEqual(CrystalTrader.Result.NoSlots, t.TryBuy("b3", T0 + 3, quota, 10, Wallet(gold)), "третьей нет");
|
||||
Assert.AreEqual(10000f, gold[0], 0.01, "золото не списано");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Trader_Quota6of6_Blocked_NoGoldSpent()
|
||||
{
|
||||
var quota = new CrystalQuota(6, 7 * Day);
|
||||
quota.Take("old", 6, T0);
|
||||
var t = new CrystalTrader(null, 7 * Day);
|
||||
var gold = new[] { 50000f };
|
||||
Assert.AreEqual(CrystalTrader.Result.NoQuota, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
||||
Assert.AreEqual(50000f, gold[0], 0.01, "6/6 + покупка -> блок без расхода золота");
|
||||
Assert.AreEqual(0, t.BoughtInWindow(T0 + 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Trader_NoSpace_Or_NoGold_NothingChanges()
|
||||
{
|
||||
var quota = new CrystalQuota(6, 7 * Day);
|
||||
var t = new CrystalTrader(null, 7 * Day);
|
||||
var gold = new[] { 50000f };
|
||||
Assert.AreEqual(CrystalTrader.Result.NoSpace, t.TryBuy("b1", T0, quota, 0, Wallet(gold)), "хранилище полное");
|
||||
Assert.AreEqual(50000f, gold[0], 0.01);
|
||||
var poor = new[] { 1000f };
|
||||
Assert.AreEqual(CrystalTrader.Result.NoGold, t.TryBuy("b2", T0, quota, 10, Wallet(poor)), "не хватает золота");
|
||||
Assert.AreEqual(0, quota.Used(T0), "квота не тронута");
|
||||
Assert.AreEqual(30000, t.PriceNow(T0), "цена по-прежнему первая");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Trader_SameOperationTwice_OnePurchase()
|
||||
{
|
||||
var quota = new CrystalQuota(6, 7 * Day);
|
||||
var t = new CrystalTrader(null, 7 * Day);
|
||||
var gold = new[] { 100000f };
|
||||
t.TryBuy("b1", T0, quota, 10, Wallet(gold));
|
||||
Assert.AreEqual(CrystalTrader.Result.Repeated, t.TryBuy("b1", T0 + 1, quota, 10, Wallet(gold)));
|
||||
Assert.AreEqual(70000f, gold[0], 0.01, "повтор той же покупки -- ничего не списано");
|
||||
Assert.AreEqual(1, quota.Used(T0 + 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Trader_WindowRolls_AndSurvivesSaveLoad()
|
||||
{
|
||||
var quota = new CrystalQuota(6, 7 * Day);
|
||||
var t = new CrystalTrader(null, 7 * Day);
|
||||
var gold = new[] { 1000000f };
|
||||
t.TryBuy("b1", T0, quota, 10, Wallet(gold));
|
||||
t.TryBuy("b2", T0 + Day, quota, 10, Wallet(gold));
|
||||
var copy = new CrystalTrader(null, 7 * Day);
|
||||
copy.Load(t.ToLines());
|
||||
Assert.AreEqual(-1, copy.PriceNow(T0 + 2 * Day), "после перезапуска -- те же 2 покупки");
|
||||
Assert.AreEqual(T0 + 7 * Day, copy.NextSlotMs(T0 + 2 * Day));
|
||||
Assert.AreEqual(60000, copy.PriceNow(T0 + 7 * Day), "первая покупка вышла из окна -- следующая снова вторая по счёту");
|
||||
Assert.AreEqual(30000, copy.PriceNow(T0 + 8 * Day), "обе вышли из окна -- снова 30 000");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +101,11 @@
|
||||
Всё ТЕСТ. Без кристаллов теперь: задания (основные и ежедневные), награды за вход (было 50 и 200), письмо-приветствие (было 100), подарок за уровень (было 10 × уровень), арена (было 5), этапы боя (было 25 на каждом 5-м), бесплатный сундук (было 15). Вместо них — золото и ресурсы.
|
||||
- **Хранилище — платное расширение (разд. 8 регламента валют, ответ 06.11, п. 1).** Старт 50; ступени (ТЕСТ): Ратуша 3 — 75 (2000 золота, 1500 дерева, 1500 камня), Ратуша 5 — 100 (4000 / 3000 / 3000 + 1000 металла), Ратуша 7 — 150 (7000 / 5000 / 5000 / 2000), Ратуша 9 — 200 (10000 / 8000 / 8000 / 4000). Ратуша только разрешает ступень, покупается в окне Ратуши («Хранилище синих кристаллов» → «Расширить до N»). Цена списывается один раз, всё или ничего. Правила — `Modules/Economy/Rules/CrystalStorage.cs`, тесты — `CrystalStorageTests`; числа — `EconomyConfig` → «Склады и лимиты» → `crystalCap`, `crystalExpansions`. Купленная ступень сохраняется в секции `"economy"` (`crystalStorageLevel`; в старом сохранении её нет — 0), вместимость по ней считается заново.
|
||||
- **Недельный PvE-предел (разд. 5.2, 10, 12 регламента валют; ответ 06.11, п. 2).** Не больше 6 новых синих кристаллов за скользящие 7 реальных суток из заданий, событий и боёв (позже — PvE-торговец). Сверх квоты — 1000 золота за кристалл (компенсация не согласована, ТЕСТ), на следующую неделю не переносится; при частичной квоте — часть кристаллами, остальное золотом. Квота и место в хранилище — разные условия: взятое по квоте, но не поместившееся ждёт места и квоту второй раз не тратит; кристаллы из письма «Кристаллы ждут места» квоту не берут. Задание берёт квоту один раз (по id). Видно в окне Ратуши: «Новых кристаллов из заданий, событий и боёв за 7 дней: N / 6». Правила — `Modules/Economy/Rules/CrystalQuota.cs`, тесты — `CrystalQuotaTests`; числа — `EconomyConfig` → `crystalPveLimit`, `crystalPveWindowDays`, `crystalCompensationGold`; сохранение — `progress.json`, `crystalQuota`. DEV: «+1 PvE-кристалл», «Сбросить квоту».
|
||||
- **Редкий PvE-торговец (разд. 3 и 10 регламента валют).** Магазин → «Редкий торговец: синий кристалл»: за скользящие 7 реальных суток первая покупка — 30 000 золота, вторая — 60 000, третьей нет (`EconomyConfig` → `crystalTraderPrices`, ТЕСТ). Покупка входит в недельный PvE-предел. Все проверки — до оплаты: нет покупок в окне, нет квоты или нет места в хранилище — блок, золото не списывается, компенсации нет. Одна покупка — один id, повтор ничего не делает; после покупки игра сразу сохраняется. Правила — `Modules/Economy/Rules/CrystalTrader.cs`, тесты — `CrystalTraderTests`; сохранение — `progress.json`, `crystalTrader`. DEV: «Золото 100 000» (мимо лимита склада, для проверки), «Сбросить квоту» сбрасывает и покупки. **Вопрос:** при лимите золота 1000 + 1000 за уровень Склада цены 30 000 / 60 000 почти недостижимы.
|
||||
- **Ускорения — за золото** (06.11, решение Саши, ТЕСТ): стройка, улучшение и обучение войск — 1 золото за 10 секунд (`EconomyConfig` → «Стройка» → `secondsPerGold`, `EconomyConfig.SpeedUpGold`); было 1 синий кристалл за 30 секунд. Подтвердить у руководителя валюту и курс.
|
||||
- **Магазин — за золото** (пакеты дерева, камня, еды, железа: 600 / 700 / 600 / 900 золота, ТЕСТ). Убраны пакеты за 50–80 кристаллов, «Мешок золота», обмен золота на кристаллы, «Горсть» (500) и «Сундук» (3000) кристаллов.
|
||||
|
||||
**Ещё не сделано:** редкий PvE-торговец (30 000 / 60 000 золота, не больше 2 за 7 суток); Кристальная шахта (до двух, 3 за 7 реальных суток, пауза при полном хранилище); космо-кристаллы; торговля и военная добыча — этап мультиплеера.
|
||||
**Ещё не сделано:** Кристальная шахта (до двух, 3 за 7 реальных суток, пауза при полном хранилище); космо-кристаллы; торговля и военная добыча — этап мультиплеера.
|
||||
|
||||
**Открыто:** окончательные цены расширения хранилища, компенсация за кристалл сверх недельного предела, уровень Ратуши для второй шахты.
|
||||
|
||||
@@ -129,6 +130,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 06.11.2026 | Claude (Саша) | Редкий PvE-торговец: синий кристалл за 30 000 / 60 000 золота, не больше 2 за 7 реальных суток, в пределах недельной квоты; блок без списания (`CrystalTrader` + 5 тестов, магазин, DEV «Золото 100 000»). |
|
||||
| 06.11.2026 | Claude (Саша) | Недельный PvE-предел синих кристаллов: 6 за 7 реальных суток, сверх — 1000 золота за кристалл (ТЕСТ); `CrystalQuota` + 6 тестов, `GameProgress.Grant/ClaimQuest`, окно Ратуши, DEV-кнопки. |
|
||||
| 06.11.2026 | Claude (Саша) | Ускорения стройки, улучшения и обучения войск — за золото: 1 золото за 10 с (`secondsPerGold`, ТЕСТ), было 1 кристалл за 30 с. |
|
||||
| 06.11.2026 | Claude (Саша) | Хранилище синих кристаллов: платное расширение 50 → 75 / 100 / 150 / 200 на Ратуше 3 / 5 / 7 / 9 в окне Ратуши (`CrystalStorage`, `EconomyConfig.crystalExpansions`, `ResourceManager.CrystalStorageLevel` в сохранении, 5 тестов). Вместимость кристаллов больше не константа 50. |
|
||||
|
||||
Reference in New Issue
Block a user