using System.Collections.Generic; using Bogatyri.World; using TMPro; using UnityEngine; using UnityEngine.UI; // Сцена карты мира: рисует замок и города, открывает окно атаки, показывает войска в походе // и итоги боёв. Правил тут нет -- они в WorldSimulation, походы ведёт MarchManager. public class WorldMapController : MonoBehaviour { [Header("Камера")] public MapCameraController mapCamera; [Header("Верхняя панель")] public TMP_Text soldiersText; public Button backButton; public Button myCityButton; [Header("Подсказка")] public TMP_Text hintText; [Header("Рейтинг")] public Button ratingButton; public RatingPanel ratingPanel; [Header("Нижняя панель (пока без функционала)")] public Button worldMapNavButton; public Button eventsNavButton; public Button circleNavButton; public Button settlementNavButton; [Header("Окно атаки")] public GameObject attackPanel; public TMP_Text attackTitleText; public TMP_Text attackInfoText; public Slider soldiersSlider; public TMP_Text soldiersToSendText; public Button sendButton; public Button cancelButton; [Header("Уведомление (итог боя / возвращение)")] public GameObject popupPanel; public TMP_Text popupTitleText; public TMP_Text popupBodyText; public Button popupOkButton; const string DefaultHint = "Нажми на замок или соседний город, чтобы напасть. Карту можно двигать и приближать"; const float CastleSize = 2.2f; const float CitySize = 1.1f; const float StartZoom = 8f; static readonly Color GroundColor = new Color(0.3f, 0.42f, 0.25f); static readonly Color CastleColor = new Color(0.55f, 0.15f, 0.15f); static readonly Color PlayerCityColor = new Color(0.25f, 0.5f, 0.95f); static readonly Color NeighborCityColor = new Color(0.75f, 0.55f, 0.3f); private Transform mapRoot; private readonly Dictionary marchViews = new Dictionary(); private readonly Queue<(string title, string body)> popupQueue = new Queue<(string, string)>(); private readonly List staleMarchIds = new List(); private MapNode selectedTarget; WorldSimulation World => MarchManager.Instance.World; void Start() { mapRoot = new GameObject("Map").transform; BuildMap(); SetUpCamera(); backButton.onClick.AddListener(SceneNavigator.ReturnToCity); if (myCityButton != null) myCityButton.onClick.AddListener(CenterOnMyCity); if (ratingButton != null) ratingButton.onClick.AddListener(ratingPanel.Open); HookUpComingSoon(worldMapNavButton, "Карта мира"); HookUpComingSoon(eventsNavButton, "События"); if (circleNavButton != null) circleNavButton.onClick.AddListener(CircleWindow.Open); // модуль Круга HookUpComingSoon(settlementNavButton, "Поселение"); sendButton.onClick.AddListener(OnSendPressed); cancelButton.onClick.AddListener(CloseAttackPanel); popupOkButton.onClick.AddListener(ShowNextPopup); soldiersSlider.onValueChanged.AddListener(_ => UpdateSoldiersToSendText()); attackPanel.SetActive(false); popupPanel.SetActive(false); if (ratingPanel != null) ratingPanel.Close(); hintText.text = DefaultHint; if (ResourceManager.Instance == null) hintText.text = "Запусти игру из сцены города (MVP1), иначе войско неоткуда брать"; } void OnEnable() { if (MarchManager.Instance == null) return; MarchManager.Instance.OnBattleResolved += HandleBattleResolved; MarchManager.Instance.OnMarchReturned += HandleMarchReturned; } void OnDisable() { if (MarchManager.Instance == null) return; MarchManager.Instance.OnBattleResolved -= HandleBattleResolved; MarchManager.Instance.OnMarchReturned -= HandleMarchReturned; } void Update() { HandleMapTap(); SyncMarchViews(); soldiersText.text = $"Войска дома: {GetSoldiersAtHome()} В походе: {World.Marches.Count}"; } void HookUpComingSoon(Button button, string sectionName) { if (button == null) return; button.onClick.AddListener(() => hintText.text = $"Раздел «{sectionName}» пока в разработке"); } // ---------------- КАРТА ---------------- void BuildMap() { float width = WorldMapLayout.HalfWidth / 1000f * 2f; float height = WorldMapLayout.HalfHeight / 1000f * 2f; CreateSprite("Ground", PrimitiveSprites.Square, GroundColor, Vector3.zero, new Vector3(width, height, 1f), -10); foreach (var node in World.Nodes) { Vector3 pos = ToWorld(node); switch (node.type) { case MapNodeType.BossCastle: 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); break; case MapNodeType.PlayerCity: // Белая рамка -- чтобы свой город сразу бросался в глаза CreateSprite(node.id + "_Frame", PrimitiveSprites.Square, Color.white, pos, Vector3.one * (CitySize + 0.25f), 0); CreateSprite(node.id, PrimitiveSprites.Square, PlayerCityColor, pos, Vector3.one * CitySize, 1); CreateLabel(node.displayName, pos - new Vector3(0, CitySize / 2f + 0.35f, 0), 3.2f); break; 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); break; } } } void SetUpCamera() { // Сцена собрана старой версией сборщика -- добавляем контроллер камеры сами if (mapCamera == null) { Camera cam = Camera.main; mapCamera = cam.GetComponent(); if (mapCamera == null) mapCamera = cam.gameObject.AddComponent(); } mapCamera.mapHalfSize = new Vector2(WorldMapLayout.HalfWidth / 1000f, WorldMapLayout.HalfHeight / 1000f); mapCamera.GetComponent().orthographicSize = StartZoom; CenterOnMyCity(); } void CenterOnMyCity() { MapNode home = World.GetNode(WorldMapLayout.PlayerCityId); if (home != null) mapCamera.CenterOn(ToWorld(home)); } SpriteRenderer CreateSprite(string name, Sprite sprite, Color color, Vector3 position, Vector3 scale, int order) { var go = new GameObject(name); go.transform.SetParent(mapRoot, false); go.transform.position = position; go.transform.localScale = scale; var sr = go.AddComponent(); sr.sprite = sprite; sr.color = color; sr.sortingOrder = order; return sr; } void CreateLabel(string text, Vector3 position, float fontSize) { var go = new GameObject("Label"); go.transform.SetParent(mapRoot, false); go.transform.position = position; var tmp = go.AddComponent(); tmp.text = text; tmp.fontSize = fontSize; tmp.alignment = TextAlignmentOptions.Center; tmp.rectTransform.sizeDelta = new Vector2(6f, 1.2f); tmp.sortingOrder = 10; } public static Vector3 ToWorld(MapNode node) { return new Vector3(node.x / 1000f, node.y / 1000f, 0f); } // ---------------- ВЫБОР ЦЕЛИ ---------------- void HandleMapTap() { if (attackPanel.activeSelf || popupPanel.activeSelf) return; if (ratingPanel != null && ratingPanel.gameObject.activeSelf) return; if (!mapCamera.TryGetTap(out Vector3 world)) return; MapNode node = FindNodeAt(world); if (node == null) return; if (node.type == MapNodeType.PlayerCity) hintText.text = "Это твой город. Нажми НАЗАД, чтобы вернуться в него"; else if (node.canBeAttacked) OpenAttackPanel(node); } MapNode FindNodeAt(Vector3 world) { foreach (var node in World.Nodes) { // Зона нажатия чуть больше самого квадрата -- по мелким городам легче попасть пальцем float halfSize = (node.type == MapNodeType.BossCastle ? CastleSize : CitySize) / 2f + 0.2f; Vector3 pos = ToWorld(node); if (Mathf.Abs(world.x - pos.x) <= halfSize && Mathf.Abs(world.y - pos.y) <= halfSize) return node; } return null; } // ---------------- ОКНО АТАКИ ---------------- void OpenAttackPanel(MapNode target) { selectedTarget = target; int available = GetSoldiersAtHome(); MapNode home = World.GetNode(WorldMapLayout.PlayerCityId); int travelSeconds = Mathf.CeilToInt(MarchRules.GetTravelTimeMs(home, target) / 1000f); attackTitleText.text = target.type == MapNodeType.NeighborCity ? $"Город игрока {target.displayName}" : target.displayName; attackInfoText.text = $"Гарнизон: {target.garrison} солдат\n" + $"Путь в одну сторону: {travelSeconds} сек\n" + $"Награда за победу: {MarchManager.FormatLoot(target.rewards)}"; soldiersSlider.minValue = available > 0 ? 1 : 0; soldiersSlider.maxValue = available; soldiersSlider.wholeNumbers = true; soldiersSlider.value = available; soldiersSlider.interactable = available > 0; sendButton.interactable = available > 0; UpdateSoldiersToSendText(); attackPanel.SetActive(true); } void UpdateSoldiersToSendText() { int available = GetSoldiersAtHome(); soldiersToSendText.text = available > 0 ? $"Отправить: {(int)soldiersSlider.value} из {available}" : "Дома нет солдат"; } void CloseAttackPanel() { attackPanel.SetActive(false); selectedTarget = null; } void OnSendPressed() { if (selectedTarget == null) return; int soldiers = (int)soldiersSlider.value; if (MarchManager.Instance.TryStartMarch(selectedTarget.id, soldiers, out string error)) { hintText.text = $"Войско выступило: {soldiers} солдат идут на {selectedTarget.displayName}"; } else { hintText.text = "Поход не начат: " + error; } CloseAttackPanel(); } static int GetSoldiersAtHome() { return ResourceManager.Instance != null ? GameServices.Get().SoldiersAtHome // [объединение] наши воины всех типов : 0; } // ---------------- ВОЙСКА НА КАРТЕ ---------------- void SyncMarchViews() { long now = MarchManager.NowMs; foreach (var march in World.Marches) { if (!marchViews.TryGetValue(march.id, out MarchView view)) { view = MarchView.Create(mapRoot, march); marchViews[march.id] = view; } view.Refresh(World, march, now); } // Убираем войска, которые уже вернулись staleMarchIds.Clear(); foreach (var id in marchViews.Keys) { bool stillActive = false; foreach (var march in World.Marches) { if (march.id == id) { stillActive = true; break; } } if (!stillActive) staleMarchIds.Add(id); } foreach (var id in staleMarchIds) { Destroy(marchViews[id].gameObject); marchViews.Remove(id); } } // ---------------- УВЕДОМЛЕНИЯ ---------------- void HandleBattleResolved(MarchData march) { MapNode target = World.GetNode(march.targetNodeId); string title = march.victory ? "ПОБЕДА" : "ПОРАЖЕНИЕ"; string body = $"{target.displayName}\n" + $"Твоё войско: {march.soldiers}, гарнизон: {march.defenders}\n" + $"Твои потери: {march.soldiers - march.survivors}, потери врага: {march.defenderLosses}\n" + $"Домой идут: {march.survivors}\n"; body += march.victory ? $"Добыча (придёт с войском): {MarchManager.FormatLoot(march.loot)}" : "Добычи нет"; EnqueuePopup(title, body); } void HandleMarchReturned(MarchData march) { string body = $"В город вернулись {march.survivors} солдат\n" + $"Добыча: {MarchManager.FormatLoot(march.loot)}"; EnqueuePopup("ВОЙСКО ДОМА", body); } void EnqueuePopup(string title, string body) { popupQueue.Enqueue((title, body)); if (!popupPanel.activeSelf) ShowNextPopup(); } void ShowNextPopup() { if (popupQueue.Count == 0) { popupPanel.SetActive(false); return; } var (title, body) = popupQueue.Dequeue(); popupTitleText.text = title; popupTitleText.color = title == "ПОРАЖЕНИЕ" ? new Color(0.9f, 0.35f, 0.3f) : new Color(1f, 0.85f, 0.3f); popupBodyText.text = body; popupPanel.SetActive(true); } }