2026-10-31 17:05:17 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
namespace Bogatyri.World
|
|
|
|
|
{
|
|
|
|
|
// ПАТРУЛИ-ЗАГЛУШКИ (Разведка М1, решение команды 31.10.2026). ВРЕМЕННАЯ ТЕСТОВАЯ МЕХАНИКА, а не ИИ соседей:
|
|
|
|
|
// соседние поселения иногда отправляют отряд к другому городу или лагерю и обратно -- чтобы проверить
|
|
|
|
|
// обнаружение, примерную численность, опознание владельца и потерю контакта.
|
|
|
|
|
// Патруль -- обычный поход (MarchData.patrol): Разведка видит его через тот же IWorldMap, что и настоящие армии.
|
|
|
|
|
// Боя, добычи и разбития лагеря нет, Армия игрока о патрулях не знает.
|
|
|
|
|
// Когда появится настоящий PvP -- Enabled = false в таблице MarchConfig, система наблюдения не меняется.
|
|
|
|
|
public sealed class PatrolRules
|
|
|
|
|
{
|
|
|
|
|
public bool Enabled = false; // в игре -- из таблицы MarchConfig (там включено для теста)
|
|
|
|
|
public int MaxActive = 3; // сколько патрулей одновременно на карте
|
|
|
|
|
public long IntervalMs = 10 * 60_000; // новый патруль -- раз в 10 РЕАЛЬНЫХ минут (если есть свободное место)
|
|
|
|
|
public int SoldiersMin = 10;
|
|
|
|
|
public int SoldiersMax = 40;
|
|
|
|
|
public int NearestSources = 6; // патрули выходят из ближайших к игроку соседних городов -- чтобы их было видно
|
|
|
|
|
public int NearestTargets = 6; // и идут к одной из ближайших к игроку целей (город или лагерь)
|
|
|
|
|
public long MaxCatchUpMs = 48L * 3_600_000; // после долгого офлайна досчитываются только последние 48 часов
|
|
|
|
|
|
|
|
|
|
public static PatrolRules Default => new PatrolRules();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Состояние патрулей в сохранении мира
|
|
|
|
|
[Serializable]
|
|
|
|
|
public class PatrolState
|
|
|
|
|
{
|
|
|
|
|
public long nextMs; // когда выйдет следующий патруль
|
|
|
|
|
public int count; // сколько патрулей уже вышло (номер броска)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed partial class WorldSimulation
|
|
|
|
|
{
|
|
|
|
|
private PatrolRules patrolRules = PatrolRules.Default;
|
|
|
|
|
private long patrolNextMs;
|
|
|
|
|
private int patrolCount;
|
|
|
|
|
|
|
|
|
|
public PatrolRules Patrols
|
|
|
|
|
{
|
|
|
|
|
get => patrolRules;
|
|
|
|
|
set => patrolRules = value ?? PatrolRules.Default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PatrolState GetPatrolState() => new PatrolState { nextMs = patrolNextMs, count = patrolCount };
|
|
|
|
|
|
|
|
|
|
public void LoadPatrolState(PatrolState s)
|
|
|
|
|
{
|
|
|
|
|
patrolNextMs = s?.nextMs ?? 0;
|
|
|
|
|
patrolCount = s?.count ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Выпустить патрули, которым пора выйти к nowMs (каждый -- со своим временем выхода).
|
|
|
|
|
// Можно звать много раз: уже вышедшие не повторяются. Выключено -- патрули убираются с карты.
|
|
|
|
|
public void SpawnPatrolsUntil(long nowMs)
|
|
|
|
|
{
|
|
|
|
|
if (!patrolRules.Enabled)
|
|
|
|
|
{
|
|
|
|
|
marches.RemoveAll(m => m.patrol);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
long interval = Math.Max(60_000, patrolRules.IntervalMs);
|
|
|
|
|
if (patrolNextMs == 0 || nowMs - patrolNextMs > patrolRules.MaxCatchUpMs)
|
|
|
|
|
patrolNextMs = patrolNextMs == 0 ? nowMs : nowMs - patrolRules.MaxCatchUpMs;
|
|
|
|
|
|
|
|
|
|
for (; patrolNextMs <= nowMs; patrolNextMs += interval)
|
|
|
|
|
{
|
|
|
|
|
long t = patrolNextMs;
|
|
|
|
|
int active = 0;
|
|
|
|
|
foreach (var m in marches) if (m.patrol && m.departMs <= t && m.returnMs > t) active++;
|
|
|
|
|
if (active >= patrolRules.MaxActive) continue;
|
|
|
|
|
SpawnPatrol(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpawnPatrol(long t)
|
|
|
|
|
{
|
|
|
|
|
var rng = new DeterministicRandom(pointSeed ^ 0x7A3201 ^ patrolCount * 104729);
|
|
|
|
|
patrolCount++;
|
|
|
|
|
|
|
|
|
|
MapNode home = GetNode(WorldMapLayout.PlayerCityId);
|
|
|
|
|
var cities = new List<MapNode>();
|
|
|
|
|
foreach (var n in nodes) if (n.type == MapNodeType.NeighborCity) cities.Add(n);
|
|
|
|
|
if (cities.Count == 0) return;
|
|
|
|
|
if (home != null) cities.Sort((a, b) => Dist(a, home).CompareTo(Dist(b, home)));
|
|
|
|
|
MapNode from = cities[rng.Range(0, Math.Min(Math.Max(1, patrolRules.NearestSources), cities.Count))];
|
|
|
|
|
|
|
|
|
|
var targets = new List<MapNode>();
|
|
|
|
|
foreach (var n in nodes)
|
|
|
|
|
if (n != from && (n.type == MapNodeType.NeighborCity || n.type == MapNodeType.Camp)) targets.Add(n);
|
|
|
|
|
if (targets.Count == 0) return;
|
|
|
|
|
// Цели -- тоже рядом с игроком: путь патруля проходит через его обзор (иначе проверять нечего)
|
|
|
|
|
MapNode center = home ?? from;
|
|
|
|
|
targets.Sort((a, b) => Dist(a, center).CompareTo(Dist(b, center)));
|
|
|
|
|
MapNode to = targets[rng.Range(0, Math.Min(Math.Max(1, patrolRules.NearestTargets), targets.Count))];
|
|
|
|
|
|
|
|
|
|
int lo = Math.Max(1, patrolRules.SoldiersMin), hi = Math.Max(lo, patrolRules.SoldiersMax);
|
|
|
|
|
int soldiers = rng.Range(lo, hi + 1);
|
|
|
|
|
long travel = TravelTimeMs(from, to, null);
|
|
|
|
|
marches.Add(new MarchData
|
|
|
|
|
{
|
|
|
|
|
id = nextMarchId++,
|
|
|
|
|
ownerId = from.ownerId,
|
|
|
|
|
fromNodeId = from.id,
|
|
|
|
|
targetNodeId = to.id,
|
|
|
|
|
targetName = to.displayName,
|
|
|
|
|
soldiers = soldiers,
|
|
|
|
|
survivors = soldiers,
|
|
|
|
|
departMs = t,
|
|
|
|
|
arriveMs = t + travel,
|
|
|
|
|
returnMs = t + 2 * travel, // боя нет -- всё время пути известно сразу
|
|
|
|
|
state = MarchState.Outbound,
|
|
|
|
|
patrol = true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 17:19:39 +01:00
|
|
|
// Продвинуть мир до nowMs по событиям: перед каждым шагом beforeStep(момент) -- Разведка досчитывает
|
|
|
|
|
// наблюдение до этого момента. Так после офлайна обратный путь войска считается уже после боя
|
|
|
|
|
// (выжившие известны), а не как "стоят у цели". Итог тот же, что у одного Advance(nowMs).
|
|
|
|
|
public void AdvanceInSteps(long nowMs, List<WorldEvent> events, Action<long> beforeStep)
|
|
|
|
|
{
|
|
|
|
|
SpawnPatrolsUntil(nowMs);
|
|
|
|
|
long next;
|
|
|
|
|
while ((next = NextEventMs(nowMs)) >= 0)
|
|
|
|
|
{
|
|
|
|
|
beforeStep?.Invoke(next);
|
|
|
|
|
Advance(next, events);
|
|
|
|
|
}
|
|
|
|
|
beforeStep?.Invoke(nowMs);
|
|
|
|
|
Advance(nowMs, events);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ближайшее событие мира не позже nowMs (приход, конец сбора, исчезновение точки, возвращение); -1 -- нет
|
|
|
|
|
public long NextEventMs(long nowMs)
|
|
|
|
|
{
|
|
|
|
|
long best = -1;
|
|
|
|
|
foreach (var m in marches)
|
|
|
|
|
{
|
|
|
|
|
long at = m.state == MarchState.Outbound ? m.arriveMs
|
|
|
|
|
: m.state == MarchState.Gathering ? m.gatherEndMs
|
|
|
|
|
: m.state == MarchState.Returning ? m.returnMs : long.MaxValue;
|
|
|
|
|
if (at <= nowMs && (best < 0 || at < best)) best = at;
|
|
|
|
|
}
|
|
|
|
|
foreach (var p in points)
|
|
|
|
|
if (p.active && p.occupiedBy == 0 && p.expireMs <= nowMs && (best < 0 || p.expireMs < best)) best = p.expireMs;
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 17:05:17 +01:00
|
|
|
static long Dist(MapNode a, MapNode b) => WorldMapLayout.Distance(a, b);
|
|
|
|
|
|
|
|
|
|
// Где войско в момент t (Unix мс) -- по времени похода, а не по текущему состоянию:
|
|
|
|
|
// Разведка досчитывает наблюдение за прошедшие минуты. false -- ещё не вышло или уже дома.
|
|
|
|
|
// x, y -- позиция, toX, toY -- куда идёт; soldiers -- сколько воинов в этот момент.
|
|
|
|
|
public bool TryGetPositionAt(MarchData m, long t, out int x, out int y, out int toX, out int toY, out int soldiers)
|
|
|
|
|
{
|
|
|
|
|
x = y = toX = toY = soldiers = 0;
|
|
|
|
|
MapNode home = GetNode(m.fromNodeId), target = GetNode(m.targetNodeId);
|
|
|
|
|
if (home == null || target == null || t < m.departMs) return false;
|
|
|
|
|
|
|
|
|
|
bool resolved = m.state == MarchState.Returning || m.state == MarchState.Finished
|
|
|
|
|
|| m.state == MarchState.Gathering || m.patrol;
|
|
|
|
|
if (t < m.arriveMs || !resolved && m.state == MarchState.Outbound && t >= m.arriveMs)
|
|
|
|
|
{
|
|
|
|
|
// В пути к цели (или дошли, а мир ещё не посчитал приход -- стоят у цели)
|
|
|
|
|
Lerp(home, target, m.departMs, m.arriveMs, Math.Min(t, m.arriveMs), out x, out y);
|
|
|
|
|
toX = target.x; toY = target.y;
|
|
|
|
|
soldiers = m.soldiers;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long backStart = m.gather ? m.gatherEndMs : m.arriveMs;
|
|
|
|
|
soldiers = m.state == MarchState.Gathering || m.gather && t < backStart ? m.soldiers : m.survivors;
|
|
|
|
|
if (soldiers <= 0) return false; // все пали в бою -- армии на карте нет
|
|
|
|
|
if (m.state == MarchState.Gathering || t < backStart)
|
|
|
|
|
{
|
|
|
|
|
x = target.x; y = target.y; toX = target.x; toY = target.y; // стоят на точке и собирают
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (m.returnMs > 0 && t >= m.returnMs) return false; // дома
|
|
|
|
|
Lerp(target, home, backStart, m.returnMs, t, out x, out y);
|
|
|
|
|
toX = home.x; toY = home.y;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void Lerp(MapNode a, MapNode b, long start, long end, long t, out int x, out int y)
|
|
|
|
|
{
|
|
|
|
|
if (end <= start) { x = b.x; y = b.y; return; }
|
|
|
|
|
long p = Math.Max(0, Math.Min(end - start, t - start));
|
|
|
|
|
x = a.x + (int)((long)(b.x - a.x) * p / (end - start));
|
|
|
|
|
y = a.y + (int)((long)(b.y - a.y) * p / (end - start));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|