using System; using System.Collections.Generic; using Bogatyri.World; using UnityEngine; // МОДУЛЬ ГЛОБАЛЬНОГО МИРА: карта и походы. Документ: Docs/Modules/World.md // Живёт между сценами (создаётся сам при запуске игры), поэтому войско идёт и возвращается, // даже пока игрок в городе. // // С другими модулями -- только через контракты и события: // войска -> IArmy (забрать в поход, вернуть выживших) // добыча -> IEconomy // бой -> IBattleSystem (передаётся в WorldSimulation) // сохранение -> сам ISaveParticipant, секция "world" // наружу -> MarchStartedEvent, MarchBattleResolvedEvent, MarchReturnedEvent // // Точка расширения под мультиплеер: сейчас WorldSimulation крутится прямо здесь. // Позже TryStartMarch будет отправлять запрос на сервер, а походы и их итоги -- приходить с сервера. public class MarchManager : MonoBehaviour, IWorld, ISaveParticipant { public const string LocalPlayerId = LocalPlayer.Id; public static MarchManager Instance { get; private set; } // События для интерфейса карты (с полными данными похода). Модули слушают события EventBus. public event Action OnMarchStarted; public event Action OnBattleResolved; public event Action OnMarchReturned; // Время мира. В мультиплеере -- время сервера, а не телефона public static long NowMs => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); private readonly List events = new List(); private WorldSimulation world; private List pendingMarches; // загружены раньше, чем мир был создан // Мир создаётся при первом обращении: к этому моменту Боевая система уже зарегистрирована public WorldSimulation World { get { if (world == null) { world = new WorldSimulation(WorldMapLayout.CreateDefault(), GameServices.Get()); if (pendingMarches != null) world.LoadMarches(pendingMarches); pendingMarches = null; } return world; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void CreateInstance() { var go = new GameObject("MarchManager"); DontDestroyOnLoad(go); go.AddComponent(); } void Awake() { Instance = this; GameServices.Register(this); SaveRegistry.Register(this); } void OnDestroy() { if (Instance != this) return; GameServices.Unregister(this); SaveRegistry.Unregister(this); } void Update() { // Без армии и экономики некуда вернуть войско и добычу -- ждём, пока они появятся if (!GameServices.Has() || !GameServices.Has()) return; events.Clear(); World.Advance(NowMs, events); foreach (var e in events) { if (e.Type == WorldEventType.BattleResolved) HandleBattleResolved(e.March); else if (e.Type == WorldEventType.MarchReturned) HandleMarchReturned(e.March); } if (events.Count > 0 && GameServices.TryGet(out ISaveSystem save)) save.SaveNow(); } // ---------------- Контракт IWorld ---------------- public int ActiveMarches => World.Marches.Count; public bool TryStartMarch(string targetId, int soldiers, out string error) { if (!GameServices.TryGet(out IArmy army)) { error = "Нет данных о войске -- запусти игру из города"; return false; } if (army.SoldiersAtHome < soldiers) { error = "Не хватает воинов. Наймите их в Казармах"; return false; } MarchData march = World.StartMarch(LocalPlayerId, WorldMapLayout.PlayerCityId, targetId, soldiers, NowMs, out error); if (march == null) return false; // Ушедшие воины недоступны дома, пока не вернутся army.TakeForMarch(march.id, soldiers); if (GameServices.TryGet(out ISaveSystem save)) save.SaveNow(); Debug.Log($"[ПОХОД] {soldiers} солдат выступили на {World.GetNode(targetId).displayName}"); OnMarchStarted?.Invoke(march); EventBus.Publish(new MarchStartedEvent { MarchId = march.id, TargetId = targetId, Soldiers = soldiers }); return true; } void HandleBattleResolved(MarchData march) { MapNode target = World.GetNode(march.targetNodeId); Debug.Log($"[ПОХОД] Бой у {target.displayName}: {(march.victory ? "ПОБЕДА" : "поражение")}. " + $"{march.soldiers} против {march.defenders}, возвращаются {march.survivors}."); OnBattleResolved?.Invoke(march); EventBus.Publish(new MarchBattleResolvedEvent { MarchId = march.id, TargetId = march.targetNodeId, Victory = march.victory, Soldiers = march.soldiers, Survivors = march.survivors }); } // Солдаты и добыча зачисляются, только когда войско дошло до дома void HandleMarchReturned(MarchData march) { GameServices.Get().ReturnFromMarch(march.id, march.survivors); IEconomy economy = GameServices.Get(); foreach (var reward in march.loot) { if (Enum.TryParse(reward.resource, out ResourceType type)) economy.Add(type, reward.amount); else Debug.LogWarning($"[ПОХОД] Неизвестный ресурс награды: {reward.resource}"); } Debug.Log($"[ПОХОД] Войско вернулось: {march.survivors} солдат, добыча: {FormatLoot(march.loot)}"); OnMarchReturned?.Invoke(march); EventBus.Publish(new MarchReturnedEvent { MarchId = march.id, Victory = march.victory, Survivors = march.survivors, Sent = march.soldiers, TargetName = World.GetNode(march.targetNodeId)?.displayName ?? "", LootText = FormatLoot(march.loot) }); } // ---------------- Сохранение (секция "world") ---------------- [Serializable] class WorldSaveData { public List marches = new List(); } public string SaveKey => "world"; public string Capture() { return JsonUtility.ToJson(new WorldSaveData { marches = GetMarchesForSave() }); } public void Restore(string json) { LoadMarches(JsonUtility.FromJson(json)?.marches); } // Новая игра: все походы отменяются (войска сбрасывает Армия вместе с прогрессом) public void ResetState() { pendingMarches = null; if (world != null) world.LoadMarches(new List()); } public List GetMarchesForSave() { return world != null ? new List(world.Marches) : new List(pendingMarches ?? new List()); } // Загрузить походы (из сохранения или из старого progress.json). Мир может быть ещё не создан. public void LoadMarches(List saved) { if (world != null) world.LoadMarches(saved); else pendingMarches = saved; } // ---------------- ТЕКСТ ДЛЯ UI ---------------- public static string FormatLoot(List loot) { if (loot == null || loot.Count == 0) return "нет"; var parts = new List(); foreach (var r in loot) { parts.Add($"+{r.amount} {GetResourceName(r.resource)}"); } return string.Join(", ", parts); } static string GetResourceName(string resource) { switch (resource) { case "Food": return "еды"; case "Wood": return "дерева"; case "Stone": return "камня"; case "Iron": return "металла"; case "Gold": return "золота"; case "Gems": return "кристаллов"; default: return resource; } } }