Files
MVP1/Assets/Scripts/Modules/Intelligence/IntelligenceService.Covert.cs
T

158 lines
9.1 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
// РАЗВЕДКА М3: ТАЙНЫЕ ОПЕРАЦИИ (ТЗ разведки, разделы 7-8). Документ: Docs/Modules/Intelligence.md, раздел «М3».
//
// - Тайный режим -- только с технологией «Шпион» (пока переключатель IntelligenceConfig).
// - Группа: до CovertMaxSoldiers (10) пехотинцев и лучников, кавалерия и осадные -- нельзя. Герой-шпион (до 20) -- позже.
// - Не больше CovertMaxOps (2) тайных операций одновременно; они занимают обычные слоты похода.
// - Проверка обнаружения: при входе в охраняемую область и дальше каждые DetectIntervalMs (5 реальных минут), пока
// отряд в ней. Шанс -- по самой опасной области под отрядом (пересечение -- максимум, не сумма), в пределах 5-75%.
// - Бросок -- по id операции и номеру проверки: одна проверка -- один результат, загрузка его не меняет.
// - Обнаружили -- миссия прервана (без отчёта), отряд физически идёт домой (это делает Мир). Казни, плена и
// бесплатного боя нет.
// Позиции отряда и охраняемые области -- от Мира (IWorldMap), сам модуль карты и маршей не хранит.
public sealed partial class IntelligenceService
{
class CovertOp
{
public string opId, armyId;
public long lastMs; // до какого момента посчитано
public long nextCheckMs; // следующая проверка, пока отряд в области
public bool inside; // отряд в охраняемой области (вход -- сразу проверка)
public int checks; // сколько проверок было
public bool detected;
public long detectedMs;
}
private readonly List<CovertOp> covert = new List<CovertOp>();
private readonly List<GuardedZone> zones = new List<GuardedZone>();
public bool CovertEnabled => rules.CovertEnabled;
public int CovertMaxSoldiers => rules.CovertMaxSoldiers;
public int ActiveCovertOps => covert.FindAll(c => !c.detected).Count;
public string CheckCovertGroup(int[] units, int activeCovertOps)
{
if (!rules.CovertEnabled) return "Тайная разведка -- после изучения технологии «Шпион»";
if (activeCovertOps >= rules.CovertMaxOps) return $"Тайных операций уже {activeCovertOps} из {rules.CovertMaxOps}";
if (units == null || units.Length < TroopType.Count) return "Нет воинов для тайной группы";
int total = 0;
for (int i = 0; i < units.Length; i++)
{
if (units[i] <= 0) continue;
if (i != TroopType.Infantry && i != TroopType.Archers) return "Тайно -- только пехота и лучники (без кавалерии и осадных)";
total += units[i];
}
if (total <= 0) return "Нет свободных пехотинцев и лучников";
if (total > rules.CovertMaxSoldiers) return $"Тайно -- не больше {rules.CovertMaxSoldiers} пехотинцев и лучников";
return null;
}
public void StartCovert(string opId, string armyId, long startMs)
{
if (string.IsNullOrEmpty(opId) || covert.Exists(c => c.opId == opId)) return;
covert.Add(new CovertOp { opId = opId, armyId = armyId, lastMs = startMs });
}
public void EndCovert(string opId) => covert.RemoveAll(c => c.opId == opId);
// Шанс за проверку по видам областей под отрядом: максимум (не сумма), герой-шпион -10, в пределах 5-75%. Нет областей -- 0
public static int DetectChance(IntelligenceRules r, IEnumerable<GuardedZoneKind> kinds, bool heroSpy = false)
{
int best = -1;
foreach (var k in kinds)
{
int c = k == GuardedZoneKind.CircleCastle ? r.DetectCircleCastlePercent : k == GuardedZoneKind.Tower ? r.DetectTowerPercent : r.DetectSettlementPercent;
if (c > best) best = c;
}
if (best < 0) return 0;
if (heroSpy) best -= r.DetectHeroSpyMinus;
return Math.Max(r.DetectMinPercent, Math.Min(r.DetectMaxPercent, best));
}
readonly List<GuardedZoneKind> kindsHere = new List<GuardedZoneKind>();
int ChanceAt(int x, int y)
{
kindsHere.Clear();
foreach (var z in zones)
{
long r = z.Kind == GuardedZoneKind.Tower ? rules.TowerRadius : rules.SettlementRadius;
long dx = x - z.X, dy = y - z.Y;
if (dx * dx + dy * dy <= r * r) kindsHere.Add(z.Kind);
}
return kindsHere.Count == 0 ? 0 : DetectChance(rules, kindsHere);
}
public List<CovertDetection> CheckCovertUntil(long nowMs)
{
var found = new List<CovertDetection>();
IWorldMap map = worldMap();
if (map == null || covert.Count == 0) return found;
zones.Clear();
map.GetGuardedZones(zones);
long step = Math.Max(1000, rules.SampleStepMs);
foreach (var op in covert)
{
if (op.detected || nowMs <= op.lastMs) continue;
// Шаги -- по одной сетке от начала операции (lastMs всегда на ней): досчитали до середины шага, загрузили --
// следующие проверки в те же моменты. Долгий офлайн -- не больше MaxCatchUpMs (по той же сетке)
if (nowMs - op.lastMs > rules.MaxCatchUpMs) op.lastMs += (nowMs - op.lastMs - rules.MaxCatchUpMs) / step * step;
for (long t = op.lastMs + step; t <= nowMs; t += step)
{
op.lastMs = t;
if (map.TryGetArmyPosition(op.armyId, t, out int x, out int y))
{
int chance = ChanceAt(x, y);
if (chance <= 0) op.inside = false;
else
{
bool check = !op.inside || t >= op.nextCheckMs; // вход в область -- сразу, дальше по интервалу
op.inside = true;
if (check)
{
op.checks++;
op.nextCheckMs = t + rules.DetectIntervalMs;
if ((int)(Roll(op.opId, op.checks, 11) % 100) < chance)
{
op.detected = true;
op.detectedMs = t;
found.Add(new CovertDetection { OpId = op.opId, ArmyId = op.armyId, TimeMs = t, Check = op.checks });
EventBus.Publish(new SpyDetectedEvent { EventId = op.opId + "#" + op.checks, ArmyId = op.armyId, TimeMs = t });
EventBus.Publish(new MissionAbortedEvent { EventId = op.opId, ArmyId = op.armyId, TimeMs = t });
break;
}
}
}
}
}
}
return found;
}
// ---------------- Сохранение: covert=<операция>|<армия>|<посчитано>|<след. проверка>|<в области>|<проверок>|<замечен>|<когда> ----------------
void CaptureCovert(StringBuilder sb)
{
foreach (var c in covert)
sb.Append("covert=").Append(string.Join("|", new[]
{
Clean(c.opId), Clean(c.armyId), c.lastMs.ToString(CultureInfo.InvariantCulture), c.nextCheckMs.ToString(CultureInfo.InvariantCulture),
c.inside ? "1" : "0", c.checks.ToString(CultureInfo.InvariantCulture), c.detected ? "1" : "0", c.detectedMs.ToString(CultureInfo.InvariantCulture)
})).Append('\n');
}
void RestoreCovert(string[] p)
{
if (p.Length < 8 || string.IsNullOrEmpty(p[0]) || covert.Exists(c => c.opId == p[0])) return;
long.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out long last);
long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long next);
int.TryParse(p[5], NumberStyles.Integer, CultureInfo.InvariantCulture, out int checks);
long.TryParse(p[7], NumberStyles.Integer, CultureInfo.InvariantCulture, out long when);
covert.Add(new CovertOp { opId = p[0], armyId = p[1], lastMs = last, nextCheckMs = next, inside = p[4] == "1", checks = checks, detected = p[6] == "1", detectedMs = when });
}
}