Files
MVP1/Assets/Scripts/Modules/World/Simulation/WorldSimulation.cs
T

199 lines
8.4 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
namespace Bogatyri.World
{
public enum MarchState
{
Outbound, // идут к цели
Returning, // бой прошёл, возвращаются домой
Finished // вернулись, солдаты и добыча зачислены
}
// Один поход. Всё время -- абсолютное (Unix-время в миллисекундах), а не "осталось N секунд":
// поход продолжается, пока игра закрыта, и одинаково понимается клиентом и сервером.
[Serializable]
public class MarchData
{
public int id;
public string ownerId;
public string fromNodeId;
public string targetNodeId;
public int soldiers; // сколько ушло
public int survivors; // сколько возвращается (известно после боя)
public bool victory;
public int defenders; // гарнизон цели в момент боя
public int defenderLosses; // сколько потерял защитник
public List<RewardEntry> loot = new List<RewardEntry>();
public long departMs;
public long arriveMs;
public long returnMs; // известно после боя
public MarchState state;
}
public static class MarchRules
{
// Скорость войска: 400 милли-единиц (0,4 клетки) в секунду.
// Ближний сосед (~4 клетки) -- около 10 сек, замок (~10 клеток) -- около 25 сек, край карты -- до минуты.
public const int SpeedPerSecond = 400;
public static long GetTravelTimeMs(MapNode from, MapNode to)
{
return (long)WorldMapLayout.Distance(from, to) * 1000 / SpeedPerSecond;
}
}
public enum WorldEventType
{
BattleResolved, // войско дошло, бой прошёл
MarchReturned // войско вернулось домой
}
public struct WorldEvent
{
public WorldEventType Type;
public MarchData March;
}
// Состояние мира: карта + походы. Не зависит от Unity -- в мультиплеере этот же код крутится на сервере,
// а клиент только показывает то, что он вернул.
public sealed class WorldSimulation
{
private readonly List<MapNode> nodes;
private readonly IBattleSystem battleSystem;
private readonly List<MarchData> marches = new List<MarchData>();
private int nextMarchId = 1;
public IReadOnlyList<MapNode> Nodes => nodes;
public IReadOnlyList<MarchData> Marches => marches;
// Бой считает модуль Боевой системы -- Мир получает его через контракт, а не знает формулу сам
public WorldSimulation(List<MapNode> nodes, IBattleSystem battleSystem)
{
this.nodes = nodes;
this.battleSystem = battleSystem;
}
public MapNode GetNode(string id)
{
return WorldMapLayout.Find(nodes, id);
}
// Восстановление походов из сохранения (или из ответа сервера)
public void LoadMarches(List<MarchData> saved)
{
marches.Clear();
if (saved == null) return;
foreach (var m in saved)
{
// Поход к точке, которой больше нет на карте (карта поменялась), восстановить нельзя
if (GetNode(m.fromNodeId) == null || GetNode(m.targetNodeId) == null) continue;
marches.Add(m);
if (m.id >= nextMarchId) nextMarchId = m.id + 1;
}
}
public MarchData StartMarch(string ownerId, string fromId, string targetId, int soldiers, long nowMs, out string error)
{
MapNode from = GetNode(fromId);
MapNode target = GetNode(targetId);
if (from == null || target == null) { error = "Неизвестная точка на карте"; return null; }
if (target == from || !target.canBeAttacked) { error = $"На {target.displayName} нападать нельзя"; return null; }
if (soldiers <= 0) { error = "Нужно отправить хотя бы одного солдата"; return null; }
var march = new MarchData
{
id = nextMarchId++,
ownerId = ownerId,
fromNodeId = fromId,
targetNodeId = targetId,
soldiers = soldiers,
survivors = soldiers,
departMs = nowMs,
arriveMs = nowMs + MarchRules.GetTravelTimeMs(from, target),
state = MarchState.Outbound
};
marches.Add(march);
error = null;
return march;
}
// Продвинуть мир до момента nowMs. Если игра была закрыта долго, за один вызов поход
// может и дойти, и вернуться -- события придут в правильном порядке.
public void Advance(long nowMs, List<WorldEvent> events)
{
foreach (var m in marches)
{
if (m.state == MarchState.Outbound && nowMs >= m.arriveMs)
{
ResolveBattle(m);
events.Add(new WorldEvent { Type = WorldEventType.BattleResolved, March = m });
}
if (m.state == MarchState.Returning && nowMs >= m.returnMs)
{
m.state = MarchState.Finished;
events.Add(new WorldEvent { Type = WorldEventType.MarchReturned, March = m });
}
}
marches.RemoveAll(m => m.state == MarchState.Finished);
}
void ResolveBattle(MarchData m)
{
MapNode from = GetNode(m.fromNodeId);
MapNode target = GetNode(m.targetNodeId);
BattleOutcome outcome = battleSystem.Resolve(new BattleRequest { Attackers = m.soldiers, Defenders = target.garrison });
m.victory = outcome.Victory;
m.survivors = outcome.Survivors;
m.defenders = target.garrison;
m.defenderLosses = outcome.DefenderLosses;
// Гарнизон соседа пока не меняется: соседи -- заглушки. Когда они станут реальными игроками,
// их потери будет списывать сервер из армии того игрока.
m.loot = new List<RewardEntry>();
if (outcome.Victory)
{
foreach (var r in target.rewards)
m.loot.Add(new RewardEntry { resource = r.resource, amount = r.amount });
}
// Обратный путь отсчитывается от момента боя, а не от "сейчас" -- важно для офлайна
m.returnMs = m.arriveMs + MarchRules.GetTravelTimeMs(target, from);
m.state = MarchState.Returning;
}
// Где сейчас войско (для отрисовки). Возвращает долю пути 0..1 и точки начала/конца отрезка.
public void GetMarchSegment(MarchData m, long nowMs, out MapNode segmentFrom, out MapNode segmentTo, out double progress)
{
MapNode home = GetNode(m.fromNodeId);
MapNode target = GetNode(m.targetNodeId);
long start, end;
if (m.state == MarchState.Outbound)
{
segmentFrom = home;
segmentTo = target;
start = m.departMs;
end = m.arriveMs;
}
else
{
segmentFrom = target;
segmentTo = home;
start = m.arriveMs;
end = m.returnMs;
}
progress = end > start ? Math.Min(1.0, Math.Max(0.0, (nowMs - start) / (double)(end - start))) : 1.0;
}
}
}