2026-10-19 18:39:28 +02:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-10-19 19:07:45 +02:00
|
|
|
|
public const string LocalPlayerId = LocalPlayer.Id;
|
2026-10-19 18:39:28 +02:00
|
|
|
|
|
|
|
|
|
|
public static MarchManager Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
// События для интерфейса карты (с полными данными похода). Модули слушают события EventBus.
|
|
|
|
|
|
public event Action<MarchData> OnMarchStarted;
|
|
|
|
|
|
public event Action<MarchData> OnBattleResolved;
|
|
|
|
|
|
public event Action<MarchData> OnMarchReturned;
|
|
|
|
|
|
|
|
|
|
|
|
// Время мира. В мультиплеере -- время сервера, а не телефона
|
|
|
|
|
|
public static long NowMs => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
|
|
|
|
|
|
|
|
|
private readonly List<WorldEvent> events = new List<WorldEvent>();
|
|
|
|
|
|
private WorldSimulation world;
|
|
|
|
|
|
private List<MarchData> pendingMarches; // загружены раньше, чем мир был создан
|
|
|
|
|
|
|
|
|
|
|
|
// Мир создаётся при первом обращении: к этому моменту Боевая система уже зарегистрирована
|
|
|
|
|
|
public WorldSimulation World
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (world == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
world = new WorldSimulation(WorldMapLayout.CreateDefault(), GameServices.Get<IBattleSystem>());
|
|
|
|
|
|
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<MarchManager>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
Instance = this;
|
|
|
|
|
|
GameServices.Register<IWorld>(this);
|
|
|
|
|
|
SaveRegistry.Register(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Instance != this) return;
|
|
|
|
|
|
GameServices.Unregister<IWorld>(this);
|
|
|
|
|
|
SaveRegistry.Unregister(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Без армии и экономики некуда вернуть войско и добычу -- ждём, пока они появятся
|
|
|
|
|
|
if (!GameServices.Has<IArmy>() || !GameServices.Has<IEconomy>()) 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<IArmy>().ReturnFromMarch(march.id, march.survivors);
|
|
|
|
|
|
|
|
|
|
|
|
IEconomy economy = GameServices.Get<IEconomy>();
|
|
|
|
|
|
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);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
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)
|
|
|
|
|
|
});
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Сохранение (секция "world") ----------------
|
|
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
class WorldSaveData
|
|
|
|
|
|
{
|
|
|
|
|
|
public List<MarchData> marches = new List<MarchData>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string SaveKey => "world";
|
|
|
|
|
|
|
|
|
|
|
|
public string Capture()
|
|
|
|
|
|
{
|
|
|
|
|
|
return JsonUtility.ToJson(new WorldSaveData { marches = GetMarchesForSave() });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Restore(string json)
|
|
|
|
|
|
{
|
|
|
|
|
|
LoadMarches(JsonUtility.FromJson<WorldSaveData>(json)?.marches);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Новая игра: все походы отменяются (войска сбрасывает Армия вместе с прогрессом)
|
|
|
|
|
|
public void ResetState()
|
|
|
|
|
|
{
|
|
|
|
|
|
pendingMarches = null;
|
|
|
|
|
|
if (world != null) world.LoadMarches(new List<MarchData>());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public List<MarchData> GetMarchesForSave()
|
|
|
|
|
|
{
|
|
|
|
|
|
return world != null ? new List<MarchData>(world.Marches) : new List<MarchData>(pendingMarches ?? new List<MarchData>());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузить походы (из сохранения или из старого progress.json). Мир может быть ещё не создан.
|
|
|
|
|
|
public void LoadMarches(List<MarchData> saved)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (world != null) world.LoadMarches(saved);
|
|
|
|
|
|
else pendingMarches = saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- ТЕКСТ ДЛЯ UI ----------------
|
|
|
|
|
|
|
|
|
|
|
|
public static string FormatLoot(List<RewardEntry> loot)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (loot == null || loot.Count == 0) return "нет";
|
|
|
|
|
|
|
|
|
|
|
|
var parts = new List<string>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|