Мир по документу 02: события строго по времени, время и номер боя в событиях, единицы таймеров

Порядок событий (офлайн — в причинно-следственном порядке):
- WorldSimulation.Advance каждый раз берёт самое раннее событие — бой или возвращение;
  раньше за один вызов шли сначала все бои, потом все возвращения, и войско,
  вернувшееся раньше чужого боя, попадало в события после него
- при равном времени — бой раньше возвращения, затем по номеру похода
- WorldEvent.TimeMs — когда событие случилось, а не когда его заметили

События похода (ядро):
- TimeMs у MarchStartedEvent, MarchBattleResolvedEvent, MarchReturnedEvent
- BattleId у MarchBattleResolvedEvent

Таймеры: явные единицы (реальные секунды/минуты часов GameClock), таблица в World.md;
скорость игрового календаря (№7) не утверждена — ускорения нет.

Тесты «Готово, когда» (документ 02), наша часть: порядок после офлайна, повторный вызов
без дублей, границы таймеров (ровно в срок, не раньше), 0 и 1 солдат.
Все 46 тестов ModuleTests проходят вне Unity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-10-30 17:11:15 +01:00
co-authored by Claude Opus 5.5
parent 6772292650
commit 8e9dbdf1cf
7 changed files with 149 additions and 22 deletions
@@ -46,8 +46,10 @@ namespace Bogatyri.World
public static class MarchRules
{
// Скорость войска: 400 милли-единиц (0,4 клетки) в секунду.
// Ближний сосед (~4 клетки) -- около 10 сек, замок (~10 клеток) -- около 25 сек, край карты -- до минуты.
// Единицы (документ 02: у каждого параметра -- единицы): время мира -- РЕАЛЬНЫЕ миллисекунды
// часов GameClock (реальное время + перемотка DEV), ускорения календаря нет -- скорость не утверждена (№7).
// Скорость войска: 400 милли-единиц карты (0,4 клетки) за 1 реальную секунду.
// Ближний сосед (~4 клетки) -- около 10 сек, замок (~10 клеток) -- около 25 сек, край карты -- до минуты. ТЕСТ.
public const int SpeedPerSecond = 400;
public static long GetTravelTimeMs(MapNode from, MapNode to)
@@ -58,7 +60,7 @@ namespace Bogatyri.World
public static class CampRules
{
// Разбитый лагерь появляется снова через 30 минут (требует балансировки)
// Разбитый лагерь появляется снова через 30 РЕАЛЬНЫХ минут после боя (план от 27.10.2026: "30 реальных минут"). ТЕСТ.
public const long RespawnMs = 30L * 60 * 1000;
}
@@ -80,6 +82,7 @@ namespace Bogatyri.World
{
public WorldEventType Type;
public MarchData March;
public long TimeMs; // когда событие случилось по часам игры (а не когда его заметили)
}
// Состояние мира: карта + походы. Не зависит от Unity -- в мультиплеере этот же код крутится на сервере,
@@ -90,7 +93,6 @@ namespace Bogatyri.World
private readonly IBattleSystem battleSystem;
private readonly List<MarchData> marches = new List<MarchData>();
private readonly Dictionary<string, long> campRespawn = new Dictionary<string, long>(); // id лагеря -> когда появится
private readonly List<MarchData> arrivals = new List<MarchData>();
private int nextMarchId = 1;
public IReadOnlyList<MapNode> Nodes => nodes;
@@ -208,23 +210,37 @@ namespace Bogatyri.World
// может и дойти, и вернуться -- события придут в правильном порядке.
public void Advance(long nowMs, List<WorldEvent> events)
{
// Бои -- в порядке прихода войск: кто первым дошёл до лагеря, тот его и разбил
arrivals.Clear();
foreach (var m in marches)
if (m.state == MarchState.Outbound && nowMs >= m.arriveMs) arrivals.Add(m);
arrivals.Sort((a, b) => a.arriveMs != b.arriveMs ? a.arriveMs.CompareTo(b.arriveMs) : a.id.CompareTo(b.id));
foreach (var m in arrivals)
// События -- строго по времени (документ 02: офлайн в причинно-следственном порядке).
// Каждый раз берётся самое раннее: приход войска к цели (бой) или возвращение домой.
// Бой назначает время возвращения, поэтому возвращение того же похода может оказаться в этом же вызове.
// Кто первым дошёл до лагеря, тот его и разбил. При равном времени -- бой раньше возвращения, затем по номеру похода.
while (true)
{
ResolveBattle(m);
events.Add(new WorldEvent { Type = WorldEventType.BattleResolved, March = m });
}
foreach (var m in marches)
{
if (m.state == MarchState.Returning && nowMs >= m.returnMs)
MarchData next = null;
long nextMs = 0;
bool nextIsBattle = false;
foreach (var m in marches)
{
m.state = MarchState.Finished;
events.Add(new WorldEvent { Type = WorldEventType.MarchReturned, March = m });
bool battle = m.state == MarchState.Outbound;
if (!battle && m.state != MarchState.Returning) continue;
long at = battle ? m.arriveMs : m.returnMs;
if (at > nowMs) continue;
if (next == null || at < nextMs || at == nextMs && (battle && !nextIsBattle || battle == nextIsBattle && m.id < next.id))
{
next = m; nextMs = at; nextIsBattle = battle;
}
}
if (next == null) break;
if (nextIsBattle)
{
ResolveBattle(next);
events.Add(new WorldEvent { Type = WorldEventType.BattleResolved, March = next, TimeMs = nextMs });
}
else
{
next.state = MarchState.Finished;
events.Add(new WorldEvent { Type = WorldEventType.MarchReturned, March = next, TimeMs = nextMs });
}
}