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(); 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(); 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 }); } // Продвинуть мир до nowMs по событиям: перед каждым шагом beforeStep(момент) -- Разведка досчитывает // наблюдение до этого момента. Так после офлайна обратный путь войска считается уже после боя // (выжившие известны), а не как "стоят у цели". Итог тот же, что у одного Advance(nowMs). public void AdvanceInSteps(long nowMs, List events, Action 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; } 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)); } } }