Files
MVP1/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Patrols.cs
T
AlexanderandClaude Opus 5.5 552694f882 Разведка М1: туман, обзор, чужие армии, последний контакт; патрули-заглушки
- Новый модуль Bogatyri.Intelligence (без Unity): туман в трёх состояниях,
  обзор поселения 5 / армии 1 клетка, «Обзор» +10/20/30% (заменяет),
  примерная численность ±20%, опознание 50% на событие наблюдения,
  последний контакт 10 реальных минут, офлайн-досчёт шагами 10 с,
  сохранение в секции intelligence.
- Мир: IWorldMap (армии в момент времени), WorldAdvancingEvent,
  патрули-заглушки соседей (временно, выключаются в MarchConfig).
- Карта: IntelligenceOverlay — туман и метки чужих армий.
- Таблица IntelligenceConfig, 11 новых тестов, документы.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:05:17 +01:00

163 lines
8.9 KiB
C#

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
});
}
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));
}
}
}