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