From 4865b14ff238d0c8791de160676baa83340aff42 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Oct 2026 16:54:49 +0100 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=B0=D1=80=D0=BD=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=BD=20=D0=B3=D0=BE=D1=80=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Модуль Армии (Bogatyri.Army, без Unity): - гарнизон — часть воинов в городе: защищает город и в походы не уходит, всего воинов столько же - IArmy: GarrisonTotal, GetGarrison(type), AvailableForMarch, SetGarrison(count) - гарнизон набирается по составу армии, не больше воинов в городе; после потерь обрезается по типам - в поход уходят только свободные воины (TakeForMarch) - сохранение: строка garrison= в секции army (старые сохранения — без гарнизона); сброс очищает - TroopsChangedEvent.Garrison Интерфейс: - окно «Войска» в городе: карточка «Гарнизон города» (0, -10, -1, +1, +10, «Все») - карта мира: «Свободно / Гарнизон / В походе», ползунок похода и прогноз — только по свободным воинам - понятная ошибка, если воины есть, но все в гарнизоне Тест «один солдат в одном месте»: 30 воинов, 10 в гарнизон → 20 свободны, всего 30 (решение команды от 19.10.2026). Документы Army, Core, World, README обновлены. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Meta/Panels/TroopsPanel.cs | 24 +++++++++ Assets/Scripts/Modules/Army/ArmyService.cs | 50 +++++++++++++++++-- .../Modules/Core/Contracts/ModuleContracts.cs | 8 ++- .../Scripts/Modules/Core/Events/GameEvents.cs | 1 + Assets/Scripts/Modules/Tests/ModuleTests.cs | 35 +++++++++++++ Assets/Scripts/Modules/World/MarchManager.cs | 6 ++- Assets/Scripts/WorldMap/WorldMapController.cs | 13 +++-- Docs/Modules/Army.md | 29 ++++++++--- Docs/Modules/Core.md | 1 + Docs/Modules/README.md | 5 +- Docs/Modules/World.md | 1 + 11 files changed, 154 insertions(+), 19 deletions(-) diff --git a/Assets/Scripts/Meta/Panels/TroopsPanel.cs b/Assets/Scripts/Meta/Panels/TroopsPanel.cs index 86a152c..e2da516 100644 --- a/Assets/Scripts/Meta/Panels/TroopsPanel.cs +++ b/Assets/Scripts/Meta/Panels/TroopsPanel.cs @@ -28,6 +28,21 @@ public class TroopsPanel : BasePanel ProfilePanel.KV(army, "Сила армии с героем", GP.ArmyPower.ToString("N0")); UIKit.Button(army, "Отправить армию на карте мира", UIKit.Theme.Green, () => OpenPanel("World"), -1, 80, 26); + // --- Гарнизон: часть воинов охраняет город и в походы не уходит --- + IArmy troopsArmy = GameServices.Get(); + var garrison = UIKit.Card(list, UIKit.Theme.CardDark); + ProfilePanel.Header(garrison, "Гарнизон города"); + ProfilePanel.KV(garrison, "В гарнизоне", troopsArmy.GarrisonTotal.ToString("N0") + " из " + troopsArmy.SoldiersAtHome.ToString("N0")); + ProfilePanel.KV(garrison, "Свободны для походов", troopsArmy.AvailableForMarch.ToString("N0")); + UIKit.Text(garrison, "Гарнизон защищает город и не уходит в походы. Воины в нём не пропадают: всего воинов столько же.", 22, UIKit.Theme.TextDim); + var gRow = UIKit.Row(garrison, 80, 10); + GarrisonButton(gRow, troopsArmy, "0", 0); + GarrisonButton(gRow, troopsArmy, "-10", troopsArmy.GarrisonTotal - 10); + GarrisonButton(gRow, troopsArmy, "-1", troopsArmy.GarrisonTotal - 1); + GarrisonButton(gRow, troopsArmy, "+1", troopsArmy.GarrisonTotal + 1); + GarrisonButton(gRow, troopsArmy, "+10", troopsArmy.GarrisonTotal + 10); + GarrisonButton(gRow, troopsArmy, "Все", troopsArmy.SoldiersAtHome); + // --- Нет казарм -- нанимать негде --- if (Barracks.Count == 0) { @@ -97,6 +112,15 @@ public class TroopsPanel : BasePanel } } + // Окно перестраивается само: Армия публикует TroopsChangedEvent, GameProgress сообщает об изменении + static void GarrisonButton(Transform row, IArmy army, string label, int target) + { + target = Mathf.Clamp(target, 0, army.SoldiersAtHome); + bool can = target != army.GarrisonTotal; + var b = UIKit.Button(row, label, can ? UIKit.Theme.Blue : UIKit.Theme.Gray, () => { if (can) army.SetGarrison(target); }, -1, 80, 28); + UIKit.Size(b, -1, 80, 1); + } + protected override void Tick() { var d = GP.Data; diff --git a/Assets/Scripts/Modules/Army/ArmyService.cs b/Assets/Scripts/Modules/Army/ArmyService.cs index 297130f..3b637fc 100644 --- a/Assets/Scripts/Modules/Army/ArmyService.cs +++ b/Assets/Scripts/Modules/Army/ArmyService.cs @@ -8,6 +8,7 @@ using System.Text; // // Один солдат -- в одном месте: либо в городе (troops), либо в походе (marches). // Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам. +// Гарнизон (garrison) -- часть воинов города: они в troops, но в поход не уходят. garrison[i] <= troops[i] всегда. public sealed class ArmyService : IArmy, ISaveParticipant { public const int Types = 4; // пехота, лучники, кавалерия, осадные -- порядок как в GameData.Troops @@ -19,6 +20,7 @@ public sealed class ArmyService : IArmy, ISaveParticipant } private readonly int[] troops = new int[Types]; + private readonly int[] garrison = new int[Types]; private readonly List marches = new List(); // Секция "army" была в сохранении -- старые войска из progress.json переносить не нужно @@ -48,6 +50,28 @@ public sealed class ArmyService : IArmy, ISaveParticipant } } + public int GarrisonTotal + { + get { int n = 0; foreach (var g in garrison) n += g; return n; } + } + + public int GetGarrison(int type) + { + return type >= 0 && type < Types ? garrison[type] : 0; + } + + public int AvailableForMarch => SoldiersAtHome - GarrisonTotal; + + // Гарнизон набирается по составу армии в городе: 30 пехоты и 10 лучников, гарнизон 8 -> 6 пехоты и 2 лучника + public int SetGarrison(int count) + { + count = Math.Max(0, Math.Min(count, SoldiersAtHome)); + int[] units = count > 0 ? ArmyMath.TakeProportional(troops, count) : null; + for (int i = 0; i < Types; i++) garrison[i] = units != null ? units[i] : 0; + Changed(); + return count; + } + public void AddTroops(int type, int count) { if (type < 0 || type >= Types || count <= 0) return; @@ -67,13 +91,17 @@ public sealed class ArmyService : IArmy, ISaveParticipant troops[i] -= l; lost += l; } + ClampGarrison(); if (lost > 0) Changed(); return lost; } public bool TakeForMarch(int marchId, int count) { - int[] taken = ArmyMath.TakeProportional(troops, count); + // В поход -- только свободные воины, гарнизон остаётся в городе + var free = new int[Types]; + for (int i = 0; i < Types; i++) free[i] = troops[i] - garrison[i]; + int[] taken = ArmyMath.TakeProportional(free, count); if (taken == null) return false; for (int i = 0; i < Types; i++) troops[i] -= taken[i]; @@ -119,9 +147,14 @@ public sealed class ArmyService : IArmy, ISaveParticipant Changed(); } + void ClampGarrison() + { + for (int i = 0; i < Types; i++) garrison[i] = Math.Max(0, Math.Min(garrison[i], troops[i])); + } + void Changed() { - EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches }); + EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches, Garrison = GarrisonTotal }); } // ---------------- Перенос из старого progress.json ---------------- @@ -132,6 +165,7 @@ public sealed class ArmyService : IArmy, ISaveParticipant { if (legacyTroops != null) for (int i = 0; i < Types && i < legacyTroops.Length; i++) troops[i] = Math.Max(0, legacyTroops[i]); + Array.Clear(garrison, 0, Types); marches.Clear(); if (legacyMarches != null) @@ -149,6 +183,7 @@ public sealed class ArmyService : IArmy, ISaveParticipant // ---------------- Сохранение (секция "army") ---------------- // Модуль без Unity, поэтому формат -- простые строки: // troops=<пехота>,<лучники>,<кавалерия>,<осадные> + // garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (из troops; нет строки -- гарнизона нет) // march=:<пехота>,<лучники>,<кавалерия>,<осадные> public string SaveKey => "army"; @@ -157,6 +192,7 @@ public sealed class ArmyService : IArmy, ISaveParticipant { var sb = new StringBuilder(); sb.Append("troops=").Append(Join(troops)).Append('\n'); + sb.Append("garrison=").Append(Join(garrison)).Append('\n'); foreach (var m in marches) sb.Append("march=").Append(m.marchId.ToString(CultureInfo.InvariantCulture)).Append(':').Append(Join(m.units)).Append('\n'); return sb.ToString(); @@ -165,6 +201,7 @@ public sealed class ArmyService : IArmy, ISaveParticipant public void Restore(string data) { Array.Clear(troops, 0, Types); + Array.Clear(garrison, 0, Types); marches.Clear(); RestoredFromSave = true; @@ -178,6 +215,11 @@ public sealed class ArmyService : IArmy, ISaveParticipant int[] t = Parse(line.Substring("troops=".Length)); Array.Copy(t, troops, Types); } + else if (line.StartsWith("garrison=")) + { + int[] g = Parse(line.Substring("garrison=".Length)); + Array.Copy(g, garrison, Types); + } else if (line.StartsWith("march=")) { string[] parts = line.Substring("march=".Length).Split(':'); @@ -186,13 +228,15 @@ public sealed class ArmyService : IArmy, ISaveParticipant } } } + ClampGarrison(); // гарнизон не больше войск, даже если файл правили руками Changed(); } - // Новая игра: войск нет, походов нет + // Новая игра: войск нет, гарнизона и походов нет public void ResetState() { Array.Clear(troops, 0, Types); + Array.Clear(garrison, 0, Types); marches.Clear(); Changed(); } diff --git a/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs b/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs index 049efd7..6980fc9 100644 --- a/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs +++ b/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs @@ -55,9 +55,15 @@ public interface IArmy { int TypeCount { get; } // типов войск (пехота, лучники, кавалерия, осадные) int GetTroops(int type); // воинов этого типа в городе - int SoldiersAtHome { get; } // всего воинов в городе + int SoldiersAtHome { get; } // всего воинов в городе (вместе с гарнизоном) int TroopsInMarches { get; } // воинов в походах + // Гарнизон -- часть воинов в городе: защищает город и в походы не уходит. Всего воинов от этого не меняется. + int GarrisonTotal { get; } + int GetGarrison(int type); + int AvailableForMarch { get; } // можно отправить в поход: в городе минус гарнизон + int SetGarrison(int count); // назначить гарнизон (по составу армии); возвращает, сколько назначено + void AddTroops(int type, int count); // обучены / вернулись int LoseFraction(double fraction); // потери в бою: доля от каждого типа; возвращает, сколько погибло diff --git a/Assets/Scripts/Modules/Core/Events/GameEvents.cs b/Assets/Scripts/Modules/Core/Events/GameEvents.cs index 78a563f..4d3635f 100644 --- a/Assets/Scripts/Modules/Core/Events/GameEvents.cs +++ b/Assets/Scripts/Modules/Core/Events/GameEvents.cs @@ -54,6 +54,7 @@ public struct TroopsChangedEvent { public int AtHome; public int InMarches; + public int Garrison; // из AtHome -- в гарнизоне } // ---------------- Глобальный мир ---------------- diff --git a/Assets/Scripts/Modules/Tests/ModuleTests.cs b/Assets/Scripts/Modules/Tests/ModuleTests.cs index 16a8539..71e1268 100644 --- a/Assets/Scripts/Modules/Tests/ModuleTests.cs +++ b/Assets/Scripts/Modules/Tests/ModuleTests.cs @@ -117,6 +117,41 @@ public class ModuleTests Assert.AreEqual(0, army.SoldiersAtHome + army.TroopsInMarches); } + // Решение команды от 19.10.2026: один солдат -- в одном месте. 30 солдат, 10 в гарнизон -> 20 доступны, всего 30. + [Test] + public void ArmyService_Garrison_OneSoldierInOnePlace() + { + var army = new ArmyService(); + army.AddTroops(0, 20); army.AddTroops(1, 10); + + Assert.AreEqual(10, army.SetGarrison(10)); + Assert.AreEqual(10, army.GarrisonTotal); + Assert.AreEqual(20, army.AvailableForMarch); + Assert.AreEqual(30, army.SoldiersAtHome, "всего воинов столько же"); + Assert.AreEqual(7, army.GetGarrison(0), "гарнизон по составу: 2/3 пехоты"); + Assert.AreEqual(3, army.GetGarrison(1), "и 1/3 лучников"); + + Assert.IsFalse(army.TakeForMarch(1, 21), "гарнизон в поход не уходит"); + Assert.IsTrue(army.TakeForMarch(1, 20)); + Assert.AreEqual(0, army.AvailableForMarch); + Assert.AreEqual(10, army.GarrisonTotal, "гарнизон остался в городе"); + Assert.AreEqual(30, army.SoldiersAtHome + army.TroopsInMarches); + + Assert.AreEqual(10, army.SetGarrison(99), "гарнизон не больше воинов в городе"); + + army.LoseFraction(0.5); + Assert.LessOrEqual(army.GarrisonTotal, army.SoldiersAtHome, "после потерь гарнизон не больше войск"); + for (int i = 0; i < 4; i++) Assert.LessOrEqual(army.GetGarrison(i), army.GetTroops(i)); + + var loaded = new ArmyService(); + loaded.Restore(army.Capture()); + Assert.AreEqual(army.GarrisonTotal, loaded.GarrisonTotal, "гарнизон сохраняется"); + + loaded.Restore("troops=5,0,0,0\n"); // старое сохранение без строки гарнизона + Assert.AreEqual(0, loaded.GarrisonTotal); + Assert.AreEqual(5, loaded.AvailableForMarch); + } + // ---------------- Боевая система ---------------- [Test] diff --git a/Assets/Scripts/Modules/World/MarchManager.cs b/Assets/Scripts/Modules/World/MarchManager.cs index 673a157..4274694 100644 --- a/Assets/Scripts/Modules/World/MarchManager.cs +++ b/Assets/Scripts/Modules/World/MarchManager.cs @@ -103,9 +103,11 @@ public class MarchManager : MonoBehaviour, IWorld, ISaveParticipant error = "Нет данных о войске -- запусти игру из города"; return false; } - if (army.SoldiersAtHome < soldiers) + if (army.AvailableForMarch < soldiers) { - error = "Не хватает воинов. Наймите их в Казармах"; + error = army.GarrisonTotal > 0 + ? $"Не хватает свободных воинов: {army.GarrisonTotal} стоят в гарнизоне. Гарнизон меняется в окне «Войска»" + : "Не хватает воинов. Наймите их в Казармах"; return false; } diff --git a/Assets/Scripts/WorldMap/WorldMapController.cs b/Assets/Scripts/WorldMap/WorldMapController.cs index 81970ea..e268b4b 100644 --- a/Assets/Scripts/WorldMap/WorldMapController.cs +++ b/Assets/Scripts/WorldMap/WorldMapController.cs @@ -117,8 +117,9 @@ public class WorldMapController : MonoBehaviour RefreshCamps(); // Воины, а не отряды: "В походе: 1" читалось как один воин - int inMarches = GameServices.TryGet(out IArmy army) ? army.TroopsInMarches : 0; - soldiersText.text = $"Войска дома: {GetSoldiersAtHome()} В походе: {inMarches} (отрядов: {World.Marches.Count})"; + int inMarches = 0, garrison = 0; + if (GameServices.TryGet(out IArmy army)) { inMarches = army.TroopsInMarches; garrison = army.GarrisonTotal; } + soldiersText.text = $"Свободно: {GetSoldiersAtHome()} Гарнизон: {garrison} В походе: {inMarches} (отрядов: {World.Marches.Count})"; } void HookUpComingSoon(Button button, string sectionName) @@ -318,7 +319,9 @@ public class WorldMapController : MonoBehaviour if (size.y < 76) soldiersToSendText.rectTransform.sizeDelta = new Vector2(size.x, 76); soldiersToSendText.text = available > 0 ? $"Отправить: {(int)soldiersSlider.value} из {available}\n{PreviewText((int)soldiersSlider.value)}" - : "Дома нет солдат"; + : GameServices.TryGet(out IArmy army) && army.GarrisonTotal > 0 + ? "Свободных воинов нет: все в гарнизоне (окно «Войска» в городе)" + : "Дома нет солдат"; } // Ожидаемый исход боя: тот же расчёт, что сделает модуль Боя, с составом, который заберёт Армия @@ -327,7 +330,7 @@ public class WorldMapController : MonoBehaviour if (selectedTarget == null || soldiers <= 0) return ""; IArmy army = GameServices.Get(); var troops = new int[TroopType.Count]; - for (int i = 0; i < TroopType.Count; i++) troops[i] = army.GetTroops(i); + for (int i = 0; i < TroopType.Count; i++) troops[i] = army.GetTroops(i) - army.GetGarrison(i); // гарнизон в поход не идёт int[] units = ArmyMath.TakeProportional(troops, soldiers); if (units == null) return ""; @@ -373,7 +376,7 @@ public class WorldMapController : MonoBehaviour static int GetSoldiersAtHome() { return ResourceManager.Instance != null - ? GameServices.Get().SoldiersAtHome // [объединение] наши воины всех типов + ? GameServices.Get().AvailableForMarch // свободные воины всех типов: гарнизон охраняет город : 0; } diff --git a/Docs/Modules/Army.md b/Docs/Modules/Army.md index 74f3910..e51178d 100644 --- a/Docs/Modules/Army.md +++ b/Docs/Modules/Army.md @@ -1,6 +1,6 @@ # Армия -Войска и их характеристики: сколько воинов каждого типа в городе, сколько в походах, обучение, потери, уход в поход и возвращение. **Единственное место, где хранятся войска.** +Войска и их характеристики: сколько воинов каждого типа в городе, сколько из них в гарнизоне, сколько в походах, обучение, потери, уход в поход и возвращение. **Единственное место, где хранятся войска.** - **Сборка:** `Bogatyri.Army` (без Unity). - **Папка:** `Assets/Scripts/Modules/Army`. @@ -12,11 +12,14 @@ | --- | --- | | `TypeCount` | Типов войск — 4 (пехота, лучники, кавалерия, осадные; порядок как в `GameData.Troops`). | | `GetTroops(type)` | Воинов этого типа в городе. | -| `SoldiersAtHome` | Всего воинов в городе. | +| `SoldiersAtHome` | Всего воинов в городе (вместе с гарнизоном). | | `TroopsInMarches` | Воинов в походах. | +| `GarrisonTotal`, `GetGarrison(type)` | Сколько воинов в гарнизоне — всего и по типам. | +| `AvailableForMarch` | Свободные воины: в городе минус гарнизон. Только их можно отправить в поход. | +| `SetGarrison(count)` | Назначить гарнизон: набирается по составу армии в городе (30 пехоты и 10 лучников, гарнизон 8 → 6 пехоты и 2 лучника). Не больше, чем воинов в городе; возвращает, сколько назначено. | | `AddTroops(type, count)` | Добавить воинов (обучение в Казармах). | | `LoseFraction(fraction)` | Потери в бою: от каждого типа погибает доля (с округлением вниз); возвращает, сколько погибло. | -| `TakeForMarch(marchId, count)` | Забрать воинов в поход пропорционально составу армии и запомнить состав. | +| `TakeForMarch(marchId, count)` | Забрать свободных воинов (без гарнизона) в поход пропорционально составу и запомнить состав. | | `ReturnFromMarch(marchId, survivors)` | Вернуть выживших по тем же типам (пропорционально). | | `GetMarchUnits(marchId)` | Состав похода по типам — для боя с типами войск. | | `ReturnUnits(marchId, survivorUnits)` | Вернуть выживших точно по типам, как посчитал бой. Больше, чем ушло, не вернётся. | @@ -27,9 +30,18 @@ Воин либо в городе, либо в походе. Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам. Тест `ArmyService_MarchNeverCreatesOrLosesSoldiers` проверяет, что «дома + в походах» всегда равно общему числу. +**Гарнизон** — часть воинов города. Они по-прежнему в городе (`SoldiersAtHome`), но в поход не уходят: + +- 30 воинов, 10 в гарнизоне → 20 свободны, всего по-прежнему 30 (тест `ArmyService_Garrison_OneSoldierInOnePlace`); +- гарнизон по каждому типу никогда не больше воинов этого типа: после потерь (`LoseFraction`) он обрезается; +- новые воины из Казарм становятся свободными, гарнизон не меняется; +- назначается в окне «Войска» в городе, карточка «Гарнизон города»: кнопки 0, -10, -1, +1, +10, «Все». + +Пока гарнизон только держит воинов дома: на город игрока никто не нападает. Защита города, еда гарнизона и дебаффы при голоде — по плану ниже. + ## События -- **Публикует:** `TroopsChangedEvent` (воинов в городе и в походах) — при обучении, потерях, походе, возвращении, загрузке, сбросе. По нему `App/GameModules` перерисовывает панели HUD. +- **Публикует:** `TroopsChangedEvent` (воинов в городе, из них в гарнизоне, и в походах) — при обучении, потерях, походе, возвращении, загрузке, сбросе. По нему `App/GameModules` перерисовывает панели HUD. - **Слушает:** ничего. ## Сохранение @@ -38,10 +50,13 @@ ``` troops=<пехота>,<лучники>,<кавалерия>,<осадные> +garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (часть troops) march=:<пехота>,<лучники>,<кавалерия>,<осадные> ``` -Сброс (`ResetState`): войск и походов нет. +Старое сохранение без строки `garrison` читается: гарнизона нет. + +Сброс (`ResetState`): войск, гарнизона и походов нет. **Перенос из старых сохранений:** - Раньше войска лежали в `progress.json`: `troops` хранил `GameProgress`, `sashaMarchUnits` — мост `ArmyBridge`. @@ -64,13 +79,15 @@ march=:<пехота>,<лучники>,<кавалерия>,<о - общая армия = гарнизон + доступные отряды + войска в походах, на точках и в гарнизоне Круга, **один солдат не может быть в двух местах**; - гарнизон ест еду и участвует в защите; - при голоде дебаффы действуют и на гарнизон, и на походы. -- **Следующий шаг модуля:** гарнизон — третье место для солдата рядом с «дома» и «в походе». +- **Сделано 27.10.2026:** гарнизон города (`SetGarrison`, `AvailableForMarch`). +- **Дальше по гарнизону:** участие в защите города (когда на него смогут напасть — PvP), еда гарнизона (сейчас все воины едят одинаково, считает модуль Населения), дебаффы при голоде. Выбор состава по типам — если понадобится. - Описания типов войск (`GameData.Troops`: цена, сила, время обучения) пока в слое приложения; цены перейдут в `EconomyConfig`. ## Журнал изменений | Дата | Кто | Что | | --- | --- | --- | +| 27.10.2026 | Claude | Гарнизон города: `GarrisonTotal`, `GetGarrison`, `AvailableForMarch`, `SetGarrison`; в поход уходят только свободные; строка `garrison=` в сохранении; поле `Garrison` в `TroopsChangedEvent`; карточка «Гарнизон города» в окне «Войска». Тест «один солдат в одном месте» с гарнизоном. | | 27.10.2026 | Claude | `GetMarchUnits` и `ReturnUnits`: бой с типами войск получает состав похода, выжившие возвращаются точно по типам. | | 23.10.2026 | Claude | Войска переехали в модуль: `ArmyService` — единственное хранилище войск (секция `"army"`), `IArmy` расширен (типы, обучение, потери, войска в походах), событие `TroopsChangedEvent`. Перенос из `progress.json` с удалением старых данных только после записи `save.json`. `ArmyBridge` удалён — контракты и перенос сохранений проверены тестами. | | 19.10.2026 | Claude | `ArmyBridge.TroopsInMarches` для панели «Войска». После удаления маленькой карты войска в походах учитываются только здесь (`sashaMarchUnits`). | diff --git a/Docs/Modules/Core.md b/Docs/Modules/Core.md index 9543f06..7d311ba 100644 --- a/Docs/Modules/Core.md +++ b/Docs/Modules/Core.md @@ -56,6 +56,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 27.10.2026 | Claude | `IArmy`: гарнизон (`GarrisonTotal`, `GetGarrison`, `AvailableForMarch`, `SetGarrison`); `TroopsChangedEvent.Garrison`. | | 27.10.2026 | Claude | `MarchReturnedEvent.TargetEmpty`: войско пришло к уже разбитому лагерю. | | 27.10.2026 | Claude | `TroopType` (типы войск), `BattleRequest` / `BattleOutcome` с составом по типам, силой сторон и бонусами; `IArmy.GetMarchUnits`, `IArmy.ReturnUnits`. | | 26.10.2026 | Claude | `GameClock`; контракт `IPopulation`; события `GameTimeAdvancedEvent`, `PopulationChangedEvent`, `StarvationChangedEvent`, `NewGameStartedEvent`. | diff --git a/Docs/Modules/README.md b/Docs/Modules/README.md index 3beccb2..7c2e11e 100644 --- a/Docs/Modules/README.md +++ b/Docs/Modules/README.md @@ -47,13 +47,13 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск: **Window → General → Test Runner → EditMode → Run All**. -Уже есть: лагеря разбойников (расстановка, бой в поле, разбит и появляется снова, второе войско находит лагерь пустым, сохранение), бой с типами войск (контры, стены, осадные, потери в пределах состава), состав гарнизонов, Армия (поход не создаёт и не теряет солдат, потери по типам, сохранение, перенос старых данных, сброс), Круг (основание, вступление, выход, полный Круг, сохранение), канал «Круг» в чате только для участников, чат (пустое сообщение, обрезка длинного, событие, предел истории), деление войск без двойного учёта и без потерь, бой (больше солдат — победа, ничья — защитнику), поход с офлайн-прогрессом, шина событий (ошибка подписчика не ломает остальных), реестр модулей. +Уже есть: гарнизон города (свободные и гарнизон, в поход только свободные, потери, сохранение), лагеря разбойников (расстановка, бой в поле, разбит и появляется снова, второе войско находит лагерь пустым, сохранение), бой с типами войск (контры, стены, осадные, потери в пределах состава), состав гарнизонов, Армия (поход не создаёт и не теряет солдат, потери по типам, сохранение, перенос старых данных, сброс), Круг (основание, вступление, выход, полный Круг, сохранение), канал «Круг» в чате только для участников, чат (пустое сообщение, обрезка длинного, событие, предел истории), деление войск без двойного учёта и без потерь, бой (больше солдат — победа, ничья — защитнику), поход с офлайн-прогрессом, шина событий (ошибка подписчика не ломает остальных), реестр модулей. Добавить вместе с этапами 1, 2 и 5 (решение команды от 19.10.2026): - [x] лимит жилья: 300 жителей при лимите 300 — прирост стоит, предел вырос — прирост идёт (`PopulationTests`; пока предел растёт от Ратуши, Дома ещё нет); - [ ] нельзя призвать ниже гражданского резерва: 150 свободных, резерв 30 → призвать можно не больше 120; -- [ ] назначение гарнизона: 30 солдат, 10 в гарнизон → 20 доступны, всего по-прежнему 30 (основа — тест «один солдат в одном месте» для походов уже есть); +- [x] назначение гарнизона: 30 солдат, 10 в гарнизон → 20 доступны, всего по-прежнему 30 (`ArmyService_Garrison_OneSoldierInOnePlace`); - [ ] голод и восстановление: еда кончилась → дебаффы, в том числе у защитников; еда вернулась → снятие по правилу; - [ ] предел синих кристаллов: кристаллы не копятся бесконечно, сверх лимита — по общему правилу. @@ -61,6 +61,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск | Дата | Кто | Что | | --- | --- | --- | +| 27.10.2026 | Claude | Гарнизон города в модуле Армии. Обновлены Army, Core, World. | | 27.10.2026 | Claude | Лагеря разбойников (PvE) на карте мира. Обновлены World, Battle, Save, Core. | | 27.10.2026 | Claude | Боевая система с типами войск (контры, стены, осадные, потери по типам), состав гарнизонов на карте, ожидаемый исход в окне атаки. Обновлены Battle, World, Army, Core. | | 26.10.2026 | Claude | Модуль Населения ([Population.md](Population.md)), таблица баланса `EconomyConfig` (`Assets/Resources/EconomyConfig.asset`), часы игры `GameClock`, отладочная панель «DEV». Обновлены Core, Economy, Save. | diff --git a/Docs/Modules/World.md b/Docs/Modules/World.md index 4afdaa3..091b7dd 100644 --- a/Docs/Modules/World.md +++ b/Docs/Modules/World.md @@ -88,6 +88,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 27.10.2026 | Claude | В поход уходят только свободные воины: гарнизон остаётся в городе. Верхняя панель карты: «Свободно / Гарнизон / В походе». | | 27.10.2026 | Claude | Лагеря разбойников (PvE): 12 лагерей трёх уровней, бой в поле, после победы лагерь разбит на 30 минут, второе войско находит его пустым. Секция `world` хранит разбитые лагеря, в `MarchReturnedEvent` добавлен `TargetEmpty`. Тесты: расстановка лагерей, разбит и появляется снова, сохранение. | | 27.10.2026 | Claude | Бой с типами войск: состав похода и гарнизона, стены у замка и городов, выжившие по типам, ожидаемый исход в окне атаки. Время походов — по `GameClock` (перемотка в DEV проматывает походы). | | 23.10.2026 | Claude | Окно «ВОЙСКО ВЕРНУЛОСЬ» в городе. В `MarchReturnedEvent` добавлены `Sent`, `TargetName`, `LootText`. |