- Разведка: CheckCovertGroup, StartCovert, CheckCovertUntil (15/25/35%, при входе и каждые 5 реальных минут, максимум при пересечении, 5-75%, детерминированно по одной сетке), EndCovert; сохранение covert= - Армия: AvailableUnits, TakeUnitsForMarch -- поход точного состава - Мир: AbortMission -- домой от места обнаружения, без отчёта; охраняемые области и позиция армии через IWorldMap - Карта: «Тайно» в окне атаки и окне чужой армии, окно «РАЗВЕДЧИКОВ ЗАМЕТИЛИ» - «Шпион» -- переключатель IntelligenceConfig (ТЕСТ); 5 тестов Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
12 KiB
C#
205 lines
12 KiB
C#
using System;
|
|
|
|
namespace Bogatyri.World
|
|
{
|
|
// РАЗВЕДКА ДВИЖУЩЕЙСЯ АРМИИ (Разведка М2, ТЗ разведки, раздел 6: «вражеская армия -- 1 / 2 игровых часа»).
|
|
// Весь путь -- физический марш, без телепорта и без постоянного слежения:
|
|
// 1) отряд идёт из города к точке перехвата -- где армия окажется, когда отряд до неё дойдёт (по маршруту армии
|
|
// и скорости отряда, с шагом InterceptStepMs);
|
|
// 2) наблюдает reconHoldMs, идя рядом с армией (позиция отряда = позиция армии);
|
|
// 3) отчёт на конец наблюдения, затем домой из места, где наблюдение кончилось.
|
|
// Всё время похода известно при выступлении (маршрут армии известен Миру), поэтому офлайн считается так же.
|
|
// Догнать нельзя или армия уйдёт с карты раньше конца наблюдения -- поход не начинается (ошибка для игрока).
|
|
// Армия-цель исчезла до перехвата (патрули выключили) -- отряд дошёл до точки и вернулся (targetEmpty).
|
|
public sealed partial class WorldSimulation
|
|
{
|
|
public const long InterceptStepMs = 10_000;
|
|
|
|
// Поход ли это на армию (а не на узел карты)
|
|
public static bool IsArmyRecon(MarchData m) => m != null && m.targetMarchId > 0;
|
|
|
|
public MarchData FindMarch(int id) => marches.Find(m => m.id == id);
|
|
|
|
// Начать разведку армии. Состав ещё неизвестен -- план как для пехоты; AssignUnits пересчитает по составу
|
|
public MarchData StartArmyRecon(string ownerId, string fromId, int targetMarchId, int soldiers, long nowMs, int level, long holdMs, out string error)
|
|
{
|
|
MapNode from = GetNode(fromId);
|
|
MarchData target = FindMarch(targetMarchId);
|
|
if (from == null) { error = "Неизвестная точка на карте"; return null; }
|
|
if (target == null || !TryGetPositionAt(target, nowMs, out _, out _, out _, out _, out _)) { error = "Этой армии уже нет на карте"; return null; }
|
|
if (target.ownerId == ownerId) { error = "Это ваша армия"; return null; }
|
|
if (soldiers <= 0) { error = "Нужно отправить хотя бы одного солдата"; return null; }
|
|
if (level < 2 || level > 3 || holdMs <= 0) { error = "Уровень разведки -- II или III"; return null; }
|
|
|
|
var march = new MarchData
|
|
{
|
|
id = nextMarchId++,
|
|
ownerId = ownerId,
|
|
fromNodeId = fromId,
|
|
targetNodeId = target.targetNodeId, // узел, куда идёт армия (для старого кода, которому нужен узел)
|
|
targetMarchId = targetMarchId,
|
|
targetName = "чужая армия",
|
|
soldiers = soldiers,
|
|
survivors = soldiers,
|
|
departMs = nowMs,
|
|
state = MarchState.Outbound,
|
|
scout = true,
|
|
reconLevel = level,
|
|
reconHoldMs = holdMs,
|
|
};
|
|
if (!PlanArmyRecon(march, null, out error)) { nextMarchId--; return null; }
|
|
march.battleSeed = BattleSeed(march.id, nowMs, "march_" + targetMarchId);
|
|
marches.Add(march);
|
|
return march;
|
|
}
|
|
|
|
// Рассчитать поход без старта (для окна: «догонят через N, вернутся через M»)
|
|
public bool TryPlanArmyRecon(string fromId, int targetMarchId, long nowMs, long holdMs, out long meetMs, out long endMs, out long returnMs, out string error)
|
|
{
|
|
var probe = new MarchData { fromNodeId = fromId, targetMarchId = targetMarchId, departMs = nowMs, reconHoldMs = holdMs, scout = true, reconLevel = 2 };
|
|
bool ok = PlanArmyRecon(probe, null, out error);
|
|
meetMs = probe.arriveMs; endMs = probe.gatherEndMs; returnMs = probe.returnMs;
|
|
return ok;
|
|
}
|
|
|
|
// Перехват, наблюдение, обратный путь -- по составу units (null -- как пехота)
|
|
bool PlanArmyRecon(MarchData m, int[] units, out string error)
|
|
{
|
|
error = null;
|
|
MapNode home = GetNode(m.fromNodeId);
|
|
MarchData target = FindMarch(m.targetMarchId);
|
|
if (home == null || target == null) { error = "Этой армии уже нет на карте"; return false; }
|
|
|
|
// 1) Перехват: первый момент, когда отряд успевает дойти до места, где в этот момент будет армия
|
|
long limit = target.returnMs > 0 ? target.returnMs : m.departMs + 48L * 3_600_000;
|
|
long meet = -1;
|
|
int mx = 0, my = 0;
|
|
for (long t = m.departMs; t <= limit; t += InterceptStepMs)
|
|
{
|
|
if (!TryGetPositionAt(target, t, out int x, out int y, out _, out _, out _)) break; // армия ушла с карты
|
|
if (m.departMs + TravelTimeMs(home, Point(x, y), units) <= t) { meet = t; mx = x; my = y; break; }
|
|
}
|
|
if (meet < 0) { error = "Армию не догнать: она уйдёт раньше, чем разведчики до неё доберутся"; return false; }
|
|
|
|
// 2) Наблюдение рядом с армией: она должна оставаться на карте до конца
|
|
long end = meet + m.reconHoldMs;
|
|
if (!TryGetPositionAt(target, end, out int lx, out int ly, out _, out _, out _))
|
|
{
|
|
error = "Армия уйдёт с карты раньше, чем закончится наблюдение";
|
|
return false;
|
|
}
|
|
|
|
// 3) Домой -- из места, где наблюдение кончилось
|
|
m.arriveMs = meet;
|
|
m.meetX = mx; m.meetY = my;
|
|
m.gatherEndMs = end;
|
|
m.leaveX = lx; m.leaveY = ly;
|
|
m.returnMs = end + TravelTimeMs(Point(lx, ly), home, units);
|
|
return true;
|
|
}
|
|
|
|
// Отряд дошёл до точки перехвата: армия на месте -- наблюдает (время уже рассчитано); армии нет -- домой
|
|
void ArriveArmyRecon(MarchData m)
|
|
{
|
|
MapNode home = GetNode(m.fromNodeId);
|
|
int[] units = HasUnits(m) ? m.units : null;
|
|
m.victory = false;
|
|
m.survivors = m.soldiers;
|
|
m.survivorUnits = units != null ? (int[])units.Clone() : null;
|
|
m.loot = new System.Collections.Generic.List<RewardEntry>();
|
|
MarchData target = FindMarch(m.targetMarchId);
|
|
m.targetEmpty = target == null || !TryGetPositionAt(target, m.arriveMs, out _, out _, out _, out _, out _);
|
|
if (m.targetEmpty)
|
|
{
|
|
m.gatherEndMs = 0;
|
|
m.leaveX = m.meetX; m.leaveY = m.meetY;
|
|
m.returnMs = m.arriveMs + TravelTimeMs(Point(m.meetX, m.meetY), home, units);
|
|
m.state = MarchState.Returning;
|
|
return;
|
|
}
|
|
m.state = MarchState.Gathering; // наблюдает до gatherEndMs, обратный путь уже рассчитан
|
|
}
|
|
|
|
// Позиция отряда разведки армии в момент t
|
|
bool TryGetArmyReconPositionAt(MarchData m, long t, out int x, out int y, out int toX, out int toY, out int soldiers)
|
|
{
|
|
x = y = toX = toY = 0;
|
|
soldiers = m.soldiers;
|
|
MapNode home = GetNode(m.fromNodeId);
|
|
if (home == null || t < m.departMs) return false;
|
|
if (t < m.arriveMs)
|
|
{
|
|
LerpXY(home.x, home.y, m.meetX, m.meetY, m.departMs, m.arriveMs, t, out x, out y);
|
|
toX = m.meetX; toY = m.meetY;
|
|
return true;
|
|
}
|
|
long backStart = m.targetEmpty || m.gatherEndMs <= 0 ? m.arriveMs : m.gatherEndMs;
|
|
if (t < backStart)
|
|
{
|
|
// идёт рядом с армией; армии уже нет (не должно быть по плану) -- стоит, где кончилось наблюдение
|
|
MarchData target = FindMarch(m.targetMarchId);
|
|
if (target == null || !TryGetPositionAt(target, t, out x, out y, out toX, out toY, out _)) { x = m.leaveX; y = m.leaveY; toX = x; toY = y; }
|
|
return true;
|
|
}
|
|
if (m.returnMs > 0 && t >= m.returnMs) return false; // дома
|
|
LerpXY(m.leaveX, m.leaveY, home.x, home.y, backStart, m.returnMs, t, out x, out y);
|
|
toX = home.x; toY = home.y;
|
|
return true;
|
|
}
|
|
|
|
// Отрезок для отрисовки: точки как временные узлы карты (не входят в Nodes)
|
|
void GetArmyReconSegment(MarchData m, long nowMs, out MapNode from, out MapNode to, out double progress)
|
|
{
|
|
TryGetArmyReconPositionAt(m, nowMs, out int x, out int y, out int tx, out int ty, out _);
|
|
from = Point(x, y);
|
|
to = Point(tx, ty);
|
|
progress = 0;
|
|
}
|
|
|
|
// ---------------- Разведка М3: миссия прервана ----------------
|
|
// Тайный отряд обнаружен в момент t: миссия прервана (без отчёта и боя), отряд физически идёт домой от места,
|
|
// где был в этот момент. Только для своих разведок, которые ещё не идут домой
|
|
public bool AbortMission(MarchData m, long t)
|
|
{
|
|
if (m == null || !m.scout || m.aborted || !marches.Contains(m)) return false;
|
|
if (m.state != MarchState.Outbound && m.state != MarchState.Gathering) return false;
|
|
if (!TryGetPositionAt(m, t, out int x, out int y, out _, out _, out _)) return false;
|
|
MapNode home = GetNode(m.fromNodeId);
|
|
if (home == null) return false;
|
|
int[] units = HasUnits(m) ? m.units : null;
|
|
m.aborted = true;
|
|
m.abortMs = t;
|
|
m.leaveX = x; m.leaveY = y;
|
|
m.targetEmpty = true; // отчёта не будет
|
|
m.victory = false;
|
|
m.survivors = m.soldiers;
|
|
m.survivorUnits = units != null ? (int[])units.Clone() : null;
|
|
m.loot = new System.Collections.Generic.List<RewardEntry>();
|
|
m.state = MarchState.Returning;
|
|
m.returnMs = t + TravelTimeMs(Point(x, y), home, units);
|
|
return true;
|
|
}
|
|
|
|
bool TryGetAbortedPositionAt(MarchData m, long t, out int x, out int y, out int toX, out int toY, out int soldiers)
|
|
{
|
|
x = y = toX = toY = 0;
|
|
soldiers = m.soldiers;
|
|
MapNode home = GetNode(m.fromNodeId);
|
|
if (home == null || m.returnMs > 0 && t >= m.returnMs) return false; // дома
|
|
LerpXY(m.leaveX, m.leaveY, home.x, home.y, m.abortMs, m.returnMs, t, out x, out y);
|
|
toX = home.x; toY = home.y;
|
|
return true;
|
|
}
|
|
|
|
static MapNode Point(int x, int y) => new MapNode { id = "", x = x, y = y, displayName = "" };
|
|
|
|
static void LerpXY(int ax, int ay, int bx, int by, long start, long end, long t, out int x, out int y)
|
|
{
|
|
if (end <= start) { x = bx; y = by; return; }
|
|
long p = Math.Max(0, Math.Min(end - start, t - start));
|
|
x = ax + (int)((long)(bx - ax) * p / (end - start));
|
|
y = ay + (int)((long)(by - ay) * p / (end - start));
|
|
}
|
|
}
|
|
}
|