Разведка М2: целенаправленная разведка поселений, замка и лагерей с отчётами II и III

Отряд идёт маршем, наблюдает 2/4 (поселение, лагерь) или 3/6 (замок) игровых часов, отчёт на конец наблюдения,
потом домой. Уровень II -- точные войска, герой, стены; III -- плюс защита и ресурсы ±15%. Один отчёт на операцию,
сохранение. Карта: кнопки «Разведка II/III», окно отчёта, «Отчёты», прогноз по отчёту вне обзора.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-04 15:21:54 +01:00
co-authored by Claude Opus 5.5
parent 6c39c8b102
commit 61ba46947c
16 changed files with 552 additions and 27 deletions
+106
View File
@@ -1224,6 +1224,8 @@ public class ModuleTests
return ownerId == LocalPlayer.Id;
}
public void GetArmies(long timeMs, List<ArmyOnMap> result) => result.AddRange(Armies(timeMs));
public Dictionary<string, ReconTarget> Targets = new Dictionary<string, ReconTarget>();
public bool TryGetReconTarget(string targetId, out ReconTarget target) => Targets.TryGetValue(targetId ?? "", out target);
}
static ArmyOnMap Foreign(string id, int x, int y, int soldiers = 50) =>
@@ -1503,6 +1505,21 @@ public class ModuleTests
x = home.x; y = home.y;
return ownerId == LocalPlayer.Id;
}
public bool TryGetReconTarget(string targetId, out ReconTarget target)
{
target = default;
MapNode node = World.GetNode(targetId);
if (node == null || node.type == MapNodeType.ResourcePoint || node.type == MapNodeType.PlayerCity) return false;
target = new ReconTarget
{
Id = node.id, Name = node.displayName,
Kind = node.type == MapNodeType.BossCastle ? ReconTargetKind.Castle : node.type == MapNodeType.Camp ? ReconTargetKind.Camp : ReconTargetKind.Settlement,
Units = WorldMapLayout.GarrisonUnits(node), Fortified = WorldMapLayout.IsFortified(node),
ResourceNames = new[] { "Gold" }, ResourceAmounts = new[] { 100 }
};
return true;
}
public void GetArmies(long t, List<ArmyOnMap> result)
{
foreach (var m in World.Marches)
@@ -2076,4 +2093,93 @@ public class ModuleTests
Assert.AreEqual(hurt, copy.WoundedTotal);
Assert.IsFalse(copy.MarkMissing(1, new[] { 5, 0, 0, 0 }, 0, 0), "поход уже отмечен -- и после загрузки");
}
// ---------------- Разведка М2: целенаправленная разведка и отчёты (ТЗ разведки, раздел 6) ----------------
static FakeMap ReconMap()
{
var map = new FakeMap();
map.Targets["city"] = new ReconTarget { Id = "city", Name = "Ратибор", Kind = ReconTargetKind.Settlement,
Units = new[] { 10, 5, 3, 0 }, Fortified = true, ResourceNames = new[] { "Gold", "Food" }, ResourceAmounts = new[] { 100, 40 } };
map.Targets["castle"] = new ReconTarget { Id = "castle", Name = "Замок", Kind = ReconTargetKind.Castle, Units = new[] { 8, 6, 4, 2 }, Fortified = true };
return map;
}
[Test]
public void Recon_DurationByTargetAndLevel()
{
var intel = Intel(ReconMap());
const long H = 5 * 60_000;
Assert.AreEqual(2 * H, intel.ReconDurationMs("city", 2), "поселение, уровень II -- 2 игровых часа");
Assert.AreEqual(4 * H, intel.ReconDurationMs("city", 3));
Assert.AreEqual(3 * H, intel.ReconDurationMs("castle", 2), "замок -- 3 / 6");
Assert.AreEqual(6 * H, intel.ReconDurationMs("castle", 3));
Assert.AreEqual(0, intel.ReconDurationMs("nothing", 2), "нет такой цели");
Assert.AreEqual(0, intel.ReconDurationMs("city", 1), "уровень I -- без отчёта (это обычное наблюдение)");
}
[Test]
public void Recon_ReportLevels_OncePerOperation_Saved()
{
var map = ReconMap();
var intel = Intel(map);
var done = new List<ReconCompletedEvent>();
EventBus.Subscribe<ReconCompletedEvent>(e => done.Add(e));
Assert.IsTrue(intel.CompleteRecon("recon_1", "city", 2, 5000));
Assert.IsFalse(intel.CompleteRecon("recon_1", "city", 2, 9000), "одна операция -- один отчёт");
Assert.AreEqual(1, done.Count);
Assert.IsTrue(intel.TryGetLatestReport("city", out var r2));
Assert.AreEqual(new[] { 10, 5, 3, 0 }[0], r2.Units[0], "уровень II -- точные войска по типам");
Assert.IsTrue(r2.Fortified);
Assert.AreEqual(0, r2.ResourceNames.Length, "уровень II -- без ресурсов");
Assert.AreEqual(5000, r2.TimeMs, "время отчёта -- конец наблюдения");
// Цель изменилась после отчёта -- старый отчёт не обновляется (нет постоянного слежения)
var t = map.Targets["city"]; t.Units = new[] { 50, 0, 0, 0 }; map.Targets["city"] = t;
Assert.AreEqual(10, intel.Reports[0].Units[0]);
Assert.IsTrue(intel.CompleteRecon("recon_2", "city", 3, 7000));
intel.TryGetLatestReport("city", out var r3);
Assert.AreEqual(50, r3.Units[0], "новый отчёт -- на момент своей операции");
Assert.AreEqual(2, r3.ResourceNames.Length, "уровень III -- оценка ресурсов");
Assert.IsTrue(r3.ResourceMin[0] <= 100 && 100 <= r3.ResourceMax[0], "настоящее значение внутри окна");
Assert.AreEqual(30, r3.ResourceMax[0] - r3.ResourceMin[0], "±15% от 100 = окно 30");
StringAssertContains(r3.Modifiers, "стен");
var copy = Intel(map);
copy.Restore(intel.Capture());
Assert.AreEqual(intel.Capture(), copy.Capture(), "отчёты сохраняются");
Assert.IsFalse(copy.CompleteRecon("recon_2", "city", 3, 7000), "после загрузки отчёт не создаётся второй раз");
}
static void StringAssertContains(string text, string part) => Assert.IsTrue(text != null && text.Contains(part), $"«{text}» не содержит «{part}»");
[Test]
public void World_ReconMarch_ObservesThenReturns()
{
const long H = 5 * 60_000;
long t0 = 1_000_000_000_000;
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
var intel = Intel(new WorldAsMap { World = world });
MarchData m = world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 5, t0, out _, scout: true);
Assert.IsTrue(world.SetRecon(m, 3, intel.ReconDurationMs(WorldMapLayout.BossCastleId, 3)));
world.AssignUnits(m, new[] { 5, 0, 0, 0 });
var events = new List<WorldEvent>();
world.Advance(m.arriveMs, events);
Assert.AreEqual(WorldEventType.ScoutArrived, events[0].Type);
Assert.AreEqual(MarchState.Gathering, m.state, "стоят у цели и наблюдают");
Assert.AreEqual(m.arriveMs + 6 * H, m.gatherEndMs, "замок, уровень III -- 6 игровых часов");
Assert.IsTrue(world.TryGetPositionAt(m, m.arriveMs + H, out int x, out int y, out _, out _, out _));
MapNode boss = world.GetNode(WorldMapLayout.BossCastleId);
Assert.AreEqual(boss.x, x, "во время наблюдения -- у цели");
events.Clear();
world.Advance(m.returnMs, events);
Assert.AreEqual(WorldEventType.ReconCompleted, events[0].Type, "сначала -- отчёт");
Assert.AreEqual(WorldEventType.MarchReturned, events[1].Type, "потом -- домой");
Assert.AreEqual(5, m.survivors, "разведка без потерь");
Assert.IsTrue(intel.CompleteRecon("recon_" + m.id, m.targetNodeId, m.reconLevel, m.gatherEndMs));
}
}