From a2d85f56c398121f4bce47e0ad1c370a74fa3968 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 6 Nov 2026 16:13:43 +0100 Subject: [PATCH] =?UTF-8?q?=D0=A5=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D1=89=D0=B5=20=D1=81=D0=B8=D0=BD=D0=B8=D1=85=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B0=D0=BB=D0=BB=D0=BE=D0=B2:=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D1=82=D0=BD=D0=BE=D0=B5=20=D1=80=D0=B0=D1=81=D1=88?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BD=D0=B8=D0=B5=2050=20->=2075/100/150/200?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=A0=D0=B0=D1=82=D1=83=D1=88=D0=B5=203/5/7/?= =?UTF-8?q?9=20(=D1=80=D0=B5=D0=B3=D0=BB=D0=B0=D0=BC=D0=B5=D0=BD=D1=82=20?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D1=8E=D1=82,=20=D1=80=D0=B0=D0=B7=D0=B4.=208?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CrystalStorage (без Unity) + 5 тестов из раздела 12 - Ступени и цены -- EconomyConfig.crystalExpansions (ТЕСТ), купленная ступень -- в секции economy - Окно Ратуши: «Хранилище синих кристаллов», «Расширить до N»; цена списывается один раз Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/App/BuildingWindow.cs | 41 +++++++++ Assets/Scripts/App/EconomyConfig.cs | 67 ++++++++++++++- .../Modules/Economy/ResourceManager.cs | 9 +- .../Modules/Economy/Rules/CrystalStorage.cs | 63 ++++++++++++++ .../Economy/Rules/CrystalStorage.cs.meta | 11 +++ .../Scripts/Modules/Tests/ProductionTests.cs | 85 +++++++++++++++++++ Docs/Modules/Economy.md | 6 +- 7 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs create mode 100644 Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs.meta diff --git a/Assets/Scripts/App/BuildingWindow.cs b/Assets/Scripts/App/BuildingWindow.cs index 77f3930..34bd2fd 100644 --- a/Assets/Scripts/App/BuildingWindow.cs +++ b/Assets/Scripts/App/BuildingWindow.cs @@ -282,6 +282,8 @@ public static class BuildingWindow Live(card, () => $"Резерв для найма: {population.CivilianReserve} • можно нанять: {population.Recruitable}", 22, UIKit.Theme.TextDim); } + CrystalStorageBlock(card, level); + if (level >= townHall.maxLevel) { UIKit.Text(card, "Максимальный уровень", 26, UIKit.Theme.Gold, TextAlignmentOptions.Center); @@ -323,6 +325,45 @@ public static class BuildingWindow LiveAfford(b, canPay, () => townHall != null && townHall.CanUpgrade()); } + // Хранилище синих кристаллов (регламент валют 05.11, разд. 8; ответ 06.11, п. 1): вместимость и платное + // расширение. Ратуша только разрешает ступень -- игрок покупает её здесь за золото и обычные ресурсы + static void CrystalStorageBlock(RectTransform card, int townHallLevel) + { + var storage = EconomyConfig.CrystalStorageNow(); + Header(card, "Хранилище синих кристаллов"); + Live(card, () => + { + float have = GameServices.TryGet(out IEconomy e) ? e.Get(ResourceType.Gems) : 0f; + return $"Кристаллов {have:0} / {EconomyConfig.CrystalStorageNow().Capacity}"; + }, 24, UIKit.Theme.Text); + + var next = storage.Next; + string why = storage.WhyNot(townHallLevel); + if (next == null) + { + UIKit.Text(card, "Хранилище расширено до предела", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); + return; + } + var cost = next.Cost ?? new ResourceAmount[0]; + if (why != null) + { + UIKit.Text(card, why + ". Цена: " + CostText(cost), 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); + return; + } + LiveCost(card, cost); + bool canPay = GameServices.TryGet(out IEconomy economy) && economy.CanAfford(cost); + var b = UIKit.Button(card, $"Расширить до {next.Capacity}", canPay ? UIKit.Theme.Green : UIKit.Theme.Gray, () => + { + if (EconomyConfig.TryExpandCrystalStorage(townHallLevel, out string error)) + { + UIKit.Toast($"Хранилище кристаллов: {EconomyConfig.CrystalStorageNow().Capacity}"); + Rebuild(); + } + else UIKit.Toast(error); + }, -1, 80, 26); + LiveAfford(b, canPay, () => GameServices.TryGet(out IEconomy now) && now.CanAfford(cost)); + } + // Каких зданий на следующем уровне Ратуши можно построить больше: "Лес 2 → 3, Дом 3 → 4" static string MoreBuildingsAt(int level, int next) { diff --git a/Assets/Scripts/App/EconomyConfig.cs b/Assets/Scripts/App/EconomyConfig.cs index 5f0ce3b..d3f0894 100644 --- a/Assets/Scripts/App/EconomyConfig.cs +++ b/Assets/Scripts/App/EconomyConfig.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System; using UnityEngine; @@ -70,6 +71,20 @@ public class EconomyConfig : ScriptableObject [Range(0f, 1f)] public float famineNonFoodMultiplier = 0.5f; } + [Serializable] + public class CrystalExpansion + { + public int townHallLevel = 3; + public int capacity = 75; + public float gold, wood, stone, iron; + + public CrystalExpansion() { } + public CrystalExpansion(int townHall, int cap, float gold, float wood, float stone, float iron) + { + townHallLevel = townHall; capacity = cap; this.gold = gold; this.wood = wood; this.stone = stone; this.iron = iron; + } + } + [Serializable] public class StorageSettings // этап 1: лимиты хранения { @@ -81,8 +96,17 @@ public class EconomyConfig : ScriptableObject public float perWarehouseLevel = 1000f; [Tooltip("+ к лимиту еды за каждый уровень Амбара. Не утверждено")] public float perGranaryLevel = 1500f; - [Tooltip("Лимит синего кристалла (решение команды от 19.10: небольшой, не бесконечный). Число не утверждено")] + [Tooltip("Стартовая вместимость синих кристаллов (регламент валют 05.11, разд. 8). ТЕСТ")] public float crystalCap = 50f; + [Tooltip("Платные расширения хранилища кристаллов по порядку: с какой Ратуши, до скольки, цена (золото и обычные ресурсы). " + + "Ратуша только разрешает -- само не растёт (регламент валют 05.11, разд. 8; ответ 06.11, п. 1). Числа и цены ТЕСТ")] + public CrystalExpansion[] crystalExpansions = + { + new CrystalExpansion(3, 75, 2000, 1500, 1500, 0), + new CrystalExpansion(5, 100, 4000, 3000, 3000, 1000), + new CrystalExpansion(7, 150, 7000, 5000, 5000, 2000), + new CrystalExpansion(9, 200, 10000, 8000, 8000, 4000), + }; [Tooltip("Цена Склада: дерево / камень. Не утверждено")] public float warehouseWood = 200f; @@ -253,7 +277,7 @@ public class EconomyConfig : ScriptableObject case ResourceType.Iron: case ResourceType.Gold: return s.baseGoods + s.perWarehouseLevel * warehouseLevels; case ResourceType.Food: return s.baseFood + s.perGranaryLevel * granaryLevels; - case ResourceType.Gems: return s.crystalCap; + case ResourceType.Gems: return CrystalStorageNow().Capacity; // стартовая + купленные расширения default: return float.PositiveInfinity; } } @@ -326,4 +350,43 @@ public class EconomyConfig : ScriptableObject default: return 0f; } } + + // ---------------- Хранилище синих кристаллов (регламент валют 05.11, разд. 8; правила -- CrystalStorage) ---------------- + + // Правила с купленной ступенью (её хранит Экономика: ResourceManager.CrystalStorageLevel, секция "economy") + public static CrystalStorage CrystalStorageNow() + { + var c = Load(); + var tiers = new List(); + if (c.storage.crystalExpansions != null) + foreach (var e in c.storage.crystalExpansions) + { + if (e == null) continue; + var cost = new List(); + if (e.gold > 0) cost.Add(new ResourceAmount(ResourceType.Gold, e.gold)); + if (e.wood > 0) cost.Add(new ResourceAmount(ResourceType.Wood, e.wood)); + if (e.stone > 0) cost.Add(new ResourceAmount(ResourceType.Stone, e.stone)); + if (e.iron > 0) cost.Add(new ResourceAmount(ResourceType.Iron, e.iron)); + tiers.Add(new CrystalStorage.Tier { TownHallLevel = e.townHallLevel, Capacity = e.capacity, Cost = cost.ToArray() }); + } + var storage = new CrystalStorage(Mathf.RoundToInt(c.storage.crystalCap), tiers); + if (ResourceManager.Instance != null) storage.SetLevel(ResourceManager.Instance.CrystalStorageLevel); + return storage; + } + + // Купить следующую ступень: цена списывается один раз (всё или ничего), ступень и ресурсы -- в одной секции + // сохранения "economy", сохраняется сразу. error -- почему нельзя + public static bool TryExpandCrystalStorage(int townHallLevel, out string error) + { + var rm = ResourceManager.Instance; + var storage = CrystalStorageNow(); + error = storage.WhyNot(townHallLevel); + if (error != null) return false; + if (rm == null || !GameServices.TryGet(out IEconomy economy)) { error = "Склад не найден"; return false; } + if (!storage.TryExpand(townHallLevel, cost => economy.TrySpend(cost))) { error = "Не хватает ресурсов"; return false; } + rm.CrystalStorageLevel = storage.Level; + rm.SetCapacity(ResourceType.Gems, storage.Capacity); + if (SaveManager.Instance != null) SaveManager.Instance.SaveGame(); + return true; + } } diff --git a/Assets/Scripts/Modules/Economy/ResourceManager.cs b/Assets/Scripts/Modules/Economy/ResourceManager.cs index 0e7382a..43d5efc 100644 --- a/Assets/Scripts/Modules/Economy/ResourceManager.cs +++ b/Assets/Scripts/Modules/Economy/ResourceManager.cs @@ -30,6 +30,10 @@ public class ResourceManager : MonoBehaviour, IEconomy, ISaveParticipant // запоминаются в сохранении: на карте мира город не открыт, а лимиты действуют. private Dictionary capacities = new Dictionary(); + // Сколько платных расширений хранилища синих кристаллов куплено (регламент валют 05.11, разд. 8). + // Вместимость по ней считает слой приложения (EconomyConfig.CrystalStorageNow); здесь -- только хранится + public int CrystalStorageLevel { get; set; } + // Лимит изменился (для интерфейса: "запас / лимит") public event Action OnCapacityChanged; @@ -189,13 +193,14 @@ public class ResourceManager : MonoBehaviour, IEconomy, ISaveParticipant { public List resources = new List(); public List capacities = new List(); // лимиты склада (этап 1) + public int crystalStorageLevel; // купленные расширения хранилища кристаллов (06.11); нет в старом файле -- 0 } public string SaveKey => "economy"; public string Capture() { - var data = new EconomySaveData(); + var data = new EconomySaveData { crystalStorageLevel = CrystalStorageLevel }; foreach (var kvp in resources) data.resources.Add(new ResourceSaveEntry { type = kvp.Key, amount = kvp.Value }); foreach (var kvp in capacities) @@ -207,6 +212,7 @@ public class ResourceManager : MonoBehaviour, IEconomy, ISaveParticipant { var data = JsonUtility.FromJson(json); if (data?.resources == null) return; + CrystalStorageLevel = Mathf.Max(0, data.crystalStorageLevel); if (data.capacities != null) foreach (var entry in data.capacities) SetCapacity(entry.type, entry.amount); foreach (var entry in data.resources) SetResource(entry.type, entry.amount); @@ -215,6 +221,7 @@ public class ResourceManager : MonoBehaviour, IEconomy, ISaveParticipant // Новая игра -- стартовые значения из Inspector public void ResetState() { + CrystalStorageLevel = 0; SetStartValues(notify: true); } } diff --git a/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs b/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs new file mode 100644 index 0000000..c2603f6 --- /dev/null +++ b/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +// ХРАНИЛИЩЕ СИНИХ КРИСТАЛЛОВ: вместимость и платное расширение +// (Регламент валют, кристаллов и торговли от 05.11.2026, разд. 8; «Ответ Володе» от 06.11.2026, п. 1). +// Документ: Docs/Modules/Economy.md, раздел «Синие кристаллы». +// +// - Старт -- BaseCapacity (ТЕСТ 50). Ступени (ТЕСТ): Ратуша 3 -- 75, 5 -- 100, 7 -- 150, 9 -- 200. +// - Ступень НЕ открывается сама с уровнем Ратуши: Ратуша только разрешает её, игрок покупает за золото и +// обычные ресурсы. Ступени -- по порядку, одна покупка -- одна ступень. +// - Цена списывается один раз: ступень растёт, только если списание прошло (всё или ничего). Повторное +// нажатие без следующей ступени или без нужной Ратуши ничего не списывает. +// - Сохраняется номер купленной ступени (Level), вместимость по нему считается заново -- числа можно менять. +// Без Unity: те же правила будут на сервере и в тестах. Числа и цены даёт слой приложения (EconomyConfig). +public sealed class CrystalStorage +{ + public sealed class Tier + { + public int TownHallLevel; // с какого уровня Ратуши можно купить + public int Capacity; // вместимость после покупки + public ResourceAmount[] Cost; // цена: золото и обычные ресурсы + } + + public readonly int BaseCapacity; + readonly List tiers; + + public CrystalStorage(int baseCapacity, IEnumerable tiers) + { + BaseCapacity = Math.Max(0, baseCapacity); + this.tiers = new List(); + if (tiers != null) foreach (var t in tiers) if (t != null) this.tiers.Add(t); + } + + // Сколько ступеней куплено (0 -- только стартовая вместимость) + public int Level { get; private set; } + + public int TierCount => tiers.Count; + public int Capacity => Level <= 0 ? BaseCapacity : Math.Max(BaseCapacity, tiers[Level - 1].Capacity); + + // Следующая ступень; null -- все куплены + public Tier Next => Level < tiers.Count ? tiers[Level] : null; + + // Восстановить из сохранения (номер ступени; лишнее обрезается, если ступеней стало меньше) + public void SetLevel(int level) => Level = Math.Max(0, Math.Min(level, tiers.Count)); + + // Почему сейчас нельзя расширить: null -- можно (если хватит ресурсов -- это проверяет списание) + public string WhyNot(int townHallLevel) + { + var next = Next; + if (next == null) return "Хранилище расширено до предела"; + if (townHallLevel < next.TownHallLevel) return $"Расширение до {next.Capacity} откроется на Ратуше {next.TownHallLevel} ур."; + return null; + } + + // Купить следующую ступень. trySpend -- списание цены «всё или ничего» (Экономика). true -- ступень куплена + public bool TryExpand(int townHallLevel, Func, bool> trySpend) + { + if (WhyNot(townHallLevel) != null || trySpend == null) return false; + if (!trySpend(Next.Cost ?? new ResourceAmount[0])) return false; + Level++; + return true; + } +} diff --git a/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs.meta b/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs.meta new file mode 100644 index 0000000..b33011b --- /dev/null +++ b/Assets/Scripts/Modules/Economy/Rules/CrystalStorage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b8d2f6a1c9e47d3b5a0e7f3c2d19a85 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Modules/Tests/ProductionTests.cs b/Assets/Scripts/Modules/Tests/ProductionTests.cs index f433732..bc67923 100644 --- a/Assets/Scripts/Modules/Tests/ProductionTests.cs +++ b/Assets/Scripts/Modules/Tests/ProductionTests.cs @@ -429,3 +429,88 @@ public class CrystalRewardsTests Assert.AreEqual(10, CrystalRewards.Fits(10, 0, float.PositiveInfinity), "без предела -- целиком"); } } + +// Хранилище синих кристаллов: платное расширение по уровню Ратуши (регламент валют 05.11, разд. 8 и 12) +public class CrystalStorageTests +{ + static CrystalStorage Storage() => new CrystalStorage(50, new[] + { + new CrystalStorage.Tier { TownHallLevel = 3, Capacity = 75, Cost = new[] { new ResourceAmount(ResourceType.Gold, 2000) } }, + new CrystalStorage.Tier { TownHallLevel = 5, Capacity = 100, Cost = new[] { new ResourceAmount(ResourceType.Gold, 4000) } }, + new CrystalStorage.Tier { TownHallLevel = 7, Capacity = 150, Cost = new[] { new ResourceAmount(ResourceType.Gold, 7000) } }, + new CrystalStorage.Tier { TownHallLevel = 9, Capacity = 200, Cost = new[] { new ResourceAmount(ResourceType.Gold, 10000) } }, + }); + + [Test] + public void Storage_NotAutomatic_TownHallOnlyAllows() + { + var s = Storage(); + Assert.AreEqual(50, s.Capacity, "старт 50"); + int spent = 0; + Assert.IsFalse(s.TryExpand(2, c => { spent++; return true; }), "Ратуша 2 -- расширения нет"); + Assert.AreEqual(0, spent, "ничего не списано"); + Assert.AreEqual(50, s.Capacity, "Ратуша 3 сама вместимость не меняет -- только разрешает"); + Assert.IsNull(s.WhyNot(3)); + } + + [Test] + public void Storage_50to75_PaidOnce_AndKeptAfterSaveLoad() + { + var s = Storage(); + float gold = 2500; + System.Func, bool> pay = cost => + { + float need = 0; + foreach (var c in cost) if (c.Type == ResourceType.Gold) need += c.Amount; + if (gold < need) return false; + gold -= need; + return true; + }; + Assert.IsTrue(s.TryExpand(3, pay)); + Assert.AreEqual(75, s.Capacity, "50 -> 75 после оплаты"); + Assert.AreEqual(500, gold, 0.01, "списано один раз"); + Assert.IsFalse(s.TryExpand(3, pay), "повторное нажатие на Ратуше 3 -- следующая ступень ещё закрыта"); + Assert.AreEqual(500, gold, 0.01, "второй раз не списано"); + + var loaded = Storage(); + loaded.SetLevel(s.Level); + Assert.AreEqual(75, loaded.Capacity, "после сохранения и загрузки -- 75"); + } + + [Test] + public void Storage_NotEnoughResources_NothingChanges() + { + var s = Storage(); + Assert.IsFalse(s.TryExpand(9, c => false), "не хватило -- списание не прошло"); + Assert.AreEqual(0, s.Level); + Assert.AreEqual(50, s.Capacity); + } + + [Test] + public void Storage_TiersInOrder_UpTo200() + { + var s = Storage(); + for (int i = 0; i < 4; i++) Assert.IsTrue(s.TryExpand(10, c => true)); + Assert.AreEqual(200, s.Capacity); + Assert.IsNull(s.Next); + Assert.IsFalse(s.TryExpand(10, c => true), "дальше 200 -- нет"); + s.SetLevel(99); + Assert.AreEqual(4, s.Level, "лишние ступени из сохранения обрезаются"); + } + + [Test] + public void Storage_Waiting_ClaimedOnceAfterExpansion() + { + // 49/50 + допустимая награда 2 -> 1 сразу и 1 ждёт; после расширения ждущий забирается один раз + var s = Storage(); + var r = new CrystalRewards(); + float have = 49; + have += r.Claim("q", 2, have, s.Capacity); + Assert.AreEqual(50, have, 0.01); + Assert.AreEqual(1, r.Left("q")); + s.TryExpand(3, c => true); + have += r.Claim("q", 2, have, s.Capacity); + Assert.AreEqual(51, have, 0.01, "после расширения выдан ждущий 1"); + Assert.AreEqual(0, r.Left("q"), "больше ничего не ждёт -- выдан ровно один раз"); + } +} diff --git a/Docs/Modules/Economy.md b/Docs/Modules/Economy.md index ea66a2f..0c150b3 100644 --- a/Docs/Modules/Economy.md +++ b/Docs/Modules/Economy.md @@ -99,11 +99,12 @@ - каждый 10-й этап боя — 1 (`StageGemsEvery`, `StageGemsAmount`). Всё ТЕСТ. Без кристаллов теперь: задания (основные и ежедневные), награды за вход (было 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), вместимость по ней считается заново. - **Магазин — за золото** (пакеты дерева, камня, еды, железа: 600 / 700 / 600 / 900 золота, ТЕСТ). Убраны пакеты за 50–80 кристаллов, «Мешок золота», обмен золота на кристаллы, «Горсть» (500) и «Сундук» (3000) кристаллов. -**Ещё не сделано (после вливания `feature/3d-hud` в `develop`):** вместимость 50 → 75 / 100 / 150 / 200 с платным расширением на Ратуше 3 / 5 / 7 / 9 (сейчас константа `crystalCap` 50); недельный PvE-предел 6 с заменой излишка золотом; редкий PvE-торговец (30 000 / 60 000 золота, не больше 2 за 7 суток); Кристальная шахта (до двух, 3 за 7 реальных суток, пауза при полном хранилище); космо-кристаллы; торговля и военная добыча — этап мультиплеера. +**Ещё не сделано:** недельный PvE-предел 6 с заменой излишка золотом; редкий PvE-торговец (30 000 / 60 000 золота, не больше 2 за 7 суток); Кристальная шахта (до двух, 3 за 7 реальных суток, пауза при полном хранилище); космо-кристаллы; торговля и военная добыча — этап мультиплеера. -**Открыто:** цены расширения хранилища, компенсация за кристалл сверх недельного предела, уровень Ратуши для второй шахты, ускорения за кристаллы (обучение — 1 кристалл за 30 с, стройка) — в регламенте среди трат не названы. +**Открыто:** окончательные цены расширения хранилища, компенсация за кристалл сверх недельного предела, уровень Ратуши для второй шахты, ускорения за кристаллы (обучение — 1 кристалл за 30 с, стройка) — в регламенте среди трат не названы. ## Таблица баланса `EconomyConfig` @@ -126,6 +127,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 06.11.2026 | Claude (Саша) | Хранилище синих кристаллов: платное расширение 50 → 75 / 100 / 150 / 200 на Ратуше 3 / 5 / 7 / 9 в окне Ратуши (`CrystalStorage`, `EconomyConfig.crystalExpansions`, `ResourceManager.CrystalStorageLevel` в сохранении, 5 тестов). Вместимость кристаллов больше не константа 50. | | 06.11.2026 | Claude (Саша) | Регламент валют 05.11, разд. 2 и 5.1: синие кристаллы только за верхнюю ступень недельного события (1) и каждый 10-й этап боя (1); задания, вход, письма, уровень, арена, сундук — золото и ресурсы; магазин за золото, убраны пакеты за кристаллы, обмен золота на кристаллы, «Горсть» и «Сундук». | | 05.11.2026 | Claude (Саша) | Регламент синих кристаллов, пп. 1 и 3: кристаллы не начисляются сверх 50 и не теряются — награды заданий ждут (не больше 2), остальные — письмом «Кристаллы ждут места», из письма забирается сколько помещается (`CrystalRewards`, `GameProgress.Grant/ClaimQuest/ClaimMail`, 6 тестов). Задания — 1–2 кристалла вместо 20–250, ежедневные — без кристаллов. | | 04.11.2026 | Claude | Решения 04.11: кристаллы грабятся только нападающим с технологией «Грабёж» (`IResourceProtection.Lootable(type, attackerHasPlunder)`; с ней — не больше `plunderGemsShare` запаса, ТЕСТ 10%, точные ограничения — позже). Защита остальных ресурсов без изменений. Из `EconomyConfig` убран возврат при сносе. Офлайн: добыча здания, достроенного в офлайне, — с момента завершения; уже посчитанное время второй раз не начисляется (тесты в `ProductionTests`). |