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 covert = new List(); private readonly List zones = new List(); 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 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 kindsHere = new List(); 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 CheckCovertUntil(long nowMs) { var found = new List(); 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 }); } }