diff --git a/Assets/Scripts/App/IntelligenceConfig.cs b/Assets/Scripts/App/IntelligenceConfig.cs index f5dc575..6aa6e46 100644 --- a/Assets/Scripts/App/IntelligenceConfig.cs +++ b/Assets/Scripts/App/IntelligenceConfig.cs @@ -36,7 +36,7 @@ public class IntelligenceConfig : ScriptableObject [Header("Туман на карте")] [Tooltip("Выключено (по умолчанию): города, лагеря и точки видны всегда, туман только затемняет землю. " + - "Включено: неисследованная местность закрыта полностью вместе с объектами. Решение команды не принято")] + "Включено: неисследованная местность закрыта полностью вместе с объектами. Решения 31.10, п. 1: для М1 не требуется, оставлено переключателем")] public bool hideUnexploredObjects = false; static IntelligenceConfig cached; diff --git a/Assets/Scripts/WorldMap/WorldMapController.cs b/Assets/Scripts/WorldMap/WorldMapController.cs index e037da4..da493e0 100644 --- a/Assets/Scripts/WorldMap/WorldMapController.cs +++ b/Assets/Scripts/WorldMap/WorldMapController.cs @@ -63,6 +63,9 @@ public class WorldMapController : MonoBehaviour private readonly List staleMarchIds = new List(); private MapNode selectedTarget; + // Подписи замка и соседей: гарнизон виден только в зоне обзора (решения 31.10, п. 1) + private readonly List<(MapNode node, TextMeshPro label)> garrisonLabels = new List<(MapNode, TextMeshPro)>(); + // Лагеря меняют вид: разбит -- серый, с таймером до появления private readonly List<(MapNode node, SpriteRenderer sprite, TextMeshPro label)> campViews = new List<(MapNode, SpriteRenderer, TextMeshPro)>(); @@ -83,6 +86,7 @@ public class WorldMapController : MonoBehaviour // Окно атаки: постоянная часть текста и состояние войска, с которым оно показано private string attackInfoBase; private BattleCondition shownConditions; + private bool shownObserved; // цель была в зоне обзора, когда окно атаки открылось WorldSimulation World => MarchManager.Instance.World; @@ -185,7 +189,7 @@ public class WorldMapController : MonoBehaviour CreateSprite(node.id, PrimitiveSprites.Square, CastleColor, pos, Vector3.one * CastleSize, 0); CreateSprite(node.id + "_Keep", PrimitiveSprites.Square, new Color(0.3f, 0.08f, 0.08f), pos, Vector3.one * CastleSize * 0.5f, 1); CreateLabel(node.displayName, pos + new Vector3(0, CastleSize / 2f + 0.4f, 0), 4f); - CreateLabel($"Гарнизон: {node.garrison}", pos - new Vector3(0, CastleSize / 2f + 0.35f, 0), 3f); + garrisonLabels.Add((node, CreateLabel("", pos - new Vector3(0, CastleSize / 2f + 0.35f, 0), 3f))); break; case MapNodeType.PlayerCity: @@ -197,7 +201,7 @@ public class WorldMapController : MonoBehaviour case MapNodeType.NeighborCity: CreateSprite(node.id, PrimitiveSprites.Square, NeighborCityColor, pos, Vector3.one * CitySize, 0); - CreateLabel($"{node.displayName}\nГарнизон: {node.garrison}", pos - new Vector3(0, CitySize / 2f + 0.5f, 0), 2.6f); + garrisonLabels.Add((node, CreateLabel("", pos - new Vector3(0, CitySize / 2f + 0.5f, 0), 2.6f))); break; case MapNodeType.Camp: @@ -265,13 +269,18 @@ public class WorldMapController : MonoBehaviour long now = MarchManager.NowMs; RefreshPoints(now); + foreach (var (node, label) in garrisonLabels) + { + string garrison = GarrisonText(node); + label.text = node.type == MapNodeType.BossCastle ? garrison : $"{node.displayName}\n{garrison}"; + } foreach (var (node, sprite, label) in campViews) { long left = World.CampRespawnInMs(node.id, now); sprite.color = left > 0 ? CampClearedColor : CampColor; - label.text = left > 0 + label.text = left > 0 // разбил его сам игрок -- это он знает ? $"{node.displayName}\nразбит, вернётся через {FormatMinutes(left)}" - : $"{node.displayName}, ур. {node.level}\nГарнизон: {node.garrison}"; + : $"{node.displayName}, ур. {node.level}\n{GarrisonText(node)}"; } } @@ -294,7 +303,9 @@ public class WorldMapController : MonoBehaviour string name = World.GetNode(p.id).displayName; view.label.text = p.occupiedBy != 0 ? $"{name}\nидёт сбор" - : $"{name}\n{p.stock} {ResourceGenitive[p.kind]}, ещё {Duration(System.Math.Max(0, p.expireMs - now))}"; + : IsObserved(World.GetNode(p.id)) + ? $"{name}\n{p.stock} {ResourceGenitive[p.kind]}, ещё {Duration(System.Math.Max(0, p.expireMs - now))}" + : $"{name}\n{UnknownText}"; } // Исчезнувшие точки -- с карты @@ -312,6 +323,23 @@ public class WorldMapController : MonoBehaviour } } + // ---------------- ЗАКРЫТЫЕ СВЕДЕНИЯ (решения 31.10, п. 1) ---------------- + // Объекты видны всегда как ориентиры, а их гарнизон, запас и срок -- только пока объект в зоне обзора + // (модуль Разведки). Без Разведки (старые сцены, тесты) -- всё видно, как раньше. + + const string UnknownText = "неизвестно: вне обзора"; + + static bool IsObserved(MapNode node) + { + return node == null || !GameServices.TryGet(out IIntelligence intel) + || intel.GetVisibility(node.x, node.y) == MapVisibility.Observed; + } + + static string GarrisonText(MapNode node) + { + return IsObserved(node) ? $"Гарнизон: {node.garrison}" : $"Гарнизон: {UnknownText}"; + } + static string FormatMinutes(long ms) { long sec = (ms + 999) / 1000; @@ -371,8 +399,11 @@ public class WorldMapController : MonoBehaviour attackInfoText.enableAutoSizing = true; // строка состава гарнизона длинная -- текст ужимается, а не вылезает attackInfoText.fontSizeMin = 18; attackInfoText.fontSizeMax = 32; + shownObserved = IsObserved(target); attackInfoBase = target.type == MapNodeType.ResourcePoint ? PointInfo(target) - : $"Гарнизон: {target.garrison} ({UnitsText(WorldMapLayout.GarrisonUnits(target))})" + + : (shownObserved + ? $"Гарнизон: {target.garrison} ({UnitsText(WorldMapLayout.GarrisonUnits(target))})" + : "Гарнизон неизвестен: цель вне зоны обзора") + (WorldMapLayout.IsFortified(target) ? ", защитники за стенами\n" : ", бой в поле\n") + $"Награда за победу: {MarchManager.FormatLoot(target.rewards)}"; RefreshAttackConditions(); @@ -435,6 +466,7 @@ public class WorldMapController : MonoBehaviour void RefreshOpenAttackPanel() { if (!attackPanel.activeSelf || selectedTarget == null) return; + if (IsObserved(selectedTarget) != shownObserved) { OpenAttackPanel(selectedTarget); return; } // цель вошла в обзор или вышла if (MarchManager.PlayerConditions() == shownConditions) return; RefreshAttackConditions(); UpdateSoldiersToSendText(); @@ -463,6 +495,9 @@ public class WorldMapController : MonoBehaviour ResourcePoint p = World.GetPoint(target.id); if (p == null) return "Этой точки больше нет"; var r = World.PointRules; + if (!shownObserved) + return "Запас и срок неизвестны: точка вне зоны обзора\n" + + $"Воин добывает {r.MiningMilliPerGameHour[p.kind] / 1000f:0.#} в игровой час (5 мин), несёт {r.CargoPerSoldier[p.kind]}; осадные не несут"; return $"Запас: {p.stock} {ResourceGenitive[p.kind]}, точка исчезнет через {Duration(System.Math.Max(0, p.expireMs - MarchManager.NowMs))}\n" + $"Воин добывает {r.MiningMilliPerGameHour[p.kind] / 1000f:0.#} в игровой час (5 мин), несёт {r.CargoPerSoldier[p.kind]}; осадные не несут"; } @@ -473,6 +508,7 @@ public class WorldMapController : MonoBehaviour int[] units = PreviewUnits(soldiers); ResourcePoint p = World.GetPoint(selectedTarget.id); if (units == null || p == null) return ""; + if (!shownObserved) return "сколько унесут -- неизвестно: точка вне обзора"; MapNode home = World.GetNode(WorldMapLayout.PlayerCityId); long path = World.TravelTimeMs(home, selectedTarget, units); World.PreviewGather(p.id, units, MarchManager.NowMs + path, out int amount, out long gatherMs); @@ -487,6 +523,7 @@ public class WorldMapController : MonoBehaviour if (selectedTarget.type == MapNodeType.ResourcePoint) return GatherPreview(soldiers); int[] units = PreviewUnits(soldiers); // гарнизон в поход не идёт if (units == null) return ""; + if (!shownObserved) return "прогноз боя недоступен: гарнизон цели неизвестен"; // Прогноз с учётом случайности ±5%: показывает риск, а не итог конкретного боя (план от 27.10.2026) BattleForecast f = GameServices.Get().Forecast(new BattleRequest diff --git a/Docs/Modules/Intelligence.md b/Docs/Modules/Intelligence.md index 0770750..ff60798 100644 --- a/Docs/Modules/Intelligence.md +++ b/Docs/Modules/Intelligence.md @@ -68,7 +68,12 @@ Свои походы рисуются как раньше (`MarchView`). Чужие походы `WorldMapController` сам не рисует — только через Разведку. -**Открытый вопрос команде: что видно в неисследованных местах.** Пока не решено, по умолчанию города, лагеря и ресурсные точки видны всегда, а туман затемняет только землю. `IntelligenceConfig.hideUnexploredObjects` закрывает неисследованное вместе с объектами (тап по скрытому объекту пока работает). +**Что видно вне обзора** (решения по 8 вопросам Саши от 31.10, п. 1 — «Объекты видны, туман — для армий»): +- города, замок, лагеря и ресурсные точки видны всегда — как ориентиры, туман затемняет только землю; +- закрытые сведения — только пока объект в зоне обзора: гарнизон (на карте и в окне атаки), запас и срок ресурсной точки. Вне обзора — «неизвестно: вне обзора»; +- поэтому вне обзора нет и прогноза: ни шанса победы, ни «сколько унесут». Напасть или пойти на сбор можно; +- то, что игрок знает сам, видно всегда: лагерь разбит им и когда появится, на точке идёт его сбор; +- `IntelligenceConfig.hideUnexploredObjects` (выключено) закрывает неисследованное вместе с объектами — полное скрытие для М1 не требуется, переключатель оставлен (тап по скрытому объекту при нём работает). ## Таблица `IntelligenceConfig` @@ -100,11 +105,11 @@ ## Решения и план - М2–М4 по ТЗ: башни, герой-разведчик, разведка поселений, отчёты — следующими этапами. -- Открыто: видимость объектов в неисследованных местах (см. выше). ## Журнал изменений | Дата | Кто | Что | | --- | --- | --- | +| 01.11.2026 | Claude | Решения 31.10, п. 1: объекты видны всегда, а гарнизоны, запас и срок точек, прогноз боя и сбора — только в зоне обзора (`WorldMapController`). | | 31.10.2026 | Claude | Исправлено: после перезапуска или перемотки обратный путь войска не открывал туман (до расчёта боя Разведка считала, что войско стоит у цели). Мир досчитывается по событиям (`WorldSimulation.AdvanceInSteps`), Разведка — до каждого из них. Туман темнее. | | 31.10.2026 | Claude | Модуль создан: этап М1 — туман, источники обзора, «Обзор», чужие армии, примерная численность, опознание, последний контакт, офлайн-наблюдение, сохранение. Патрули-заглушки соседей для проверки (выключаются в `MarchConfig`). | diff --git a/Docs/Modules/README.md b/Docs/Modules/README.md index 71f32f1..85f73cd 100644 --- a/Docs/Modules/README.md +++ b/Docs/Modules/README.md @@ -105,6 +105,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск | Дата | Кто | Что | | --- | --- | --- | +| 01.11.2026 | Claude | Решения по 8 вопросам Саши (31.10), п. 1: гарнизоны и запасы точек видны только в зоне обзора. Обновлён Intelligence. | | 31.10.2026 | Claude | Окна зданий: подписи над зданиями убраны, у каждого здания (добывающие, Дом, Склад, Амбар, Ратуша, Кристалл) своё окно по тапу — состояние, добыча и склад, улучшение и его цена. Казармы тоже получили окно и уровни (открывают новые войска вместе с Ратушей; уровни 1/2/3/5 — ТЕСТ). Ратуша улучшается кнопкой в окне, а не тапом. Обновлён Settlement. | | 31.10.2026 | Claude | По регламенту 31.10: откат — Население снова самостоятельный модуль (`Modules/Population`, `Bogatyri.Population`), схема и «Кто отвечает» как у Саши. Работать может любой житель (без «40%»), резерв по уровню Ратуши 10/20/30, голод режет добычу кроме еды вдвое. Обновлены Population, Settlement, Economy, Core. | | 31.10.2026 | Claude | Модуль Разведки, этап М1 ([Intelligence.md](Intelligence.md)): туман, обзор, чужие армии, последний контакт. Патрули-заглушки соседей для проверки. Обновлены World, Core, Save. |