From 54036eafcfb4eb02dc4b9c2e42c611922616dd6b Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 31 Oct 2026 17:19:39 +0100 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B2=D0=B5=D0=B4=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=9C1:=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BF=D1=83=D1=82=D1=8C=20=D0=BE=D1=82=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D1=82=D1=83=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BE=D1=84=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мир досчитывается по событиям (AdvanceInSteps), Разведка -- до каждого: до расчёта боя войско считалось стоящим у цели всё время возвращения. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Modules/Tests/ModuleTests.cs | 55 +++++++++++++++++++ Assets/Scripts/Modules/World/MarchManager.cs | 8 +-- .../Simulation/WorldSimulation.Patrols.cs | 32 +++++++++++ Docs/Modules/Intelligence.md | 1 + 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/Modules/Tests/ModuleTests.cs b/Assets/Scripts/Modules/Tests/ModuleTests.cs index 8f8cbe9..fc148a1 100644 --- a/Assets/Scripts/Modules/Tests/ModuleTests.cs +++ b/Assets/Scripts/Modules/Tests/ModuleTests.cs @@ -1499,4 +1499,59 @@ public class ModuleTests } Assert.Greater(spotted, 0, "за сутки хотя бы один патруль прошёл рядом с поселением"); } + + [Test] + public void Intelligence_OwnMarchExploresWholePath() + { + long t0 = 1_000_000_000_000; + var world = PatrolWorld(false); + var intel = Intel(new WorldAsMap { World = world }); + MapNode home = world.GetNode(WorldMapLayout.PlayerCityId); + MapNode far = null; + foreach (var n in world.Nodes) + if (n.type == MapNodeType.NeighborCity && (far == null || WorldMapLayout.Distance(home, n) > WorldMapLayout.Distance(home, far))) far = n; + MarchData m = world.StartMarch(LocalPlayer.Id, home.id, far.id, 17, t0, out _); + world.AssignUnits(m, new[] { 17, 0, 0, 0 }); + var events = new List(); + for (long t = t0; t <= m.arriveMs + 60_000; t += 1000) + { + intel.ObserveUntil(t); + world.Advance(t, events); + } + for (int i = 0; i <= 20; i++) + { + int x = home.x + (far.x - home.x) * i / 20, y = home.y + (far.y - home.y) * i / 20; + Assert.AreNotEqual(MapVisibility.Unexplored, intel.GetVisibility(x, y), $"точка пути {i}/20 ({x},{y})"); + } + } + + [Test] + public void Intelligence_OfflineMarch_ExploresPathThereAndBack() + { + long t0 = 1_000_000_000_000; + var world = PatrolWorld(false); + var intel = Intel(new WorldAsMap { World = world }); + MapNode home = world.GetNode(WorldMapLayout.PlayerCityId); + MapNode far = null; + foreach (var n in world.Nodes) + if (n.type == MapNodeType.NeighborCity && (far == null || WorldMapLayout.Distance(home, n) > WorldMapLayout.Distance(home, far))) far = n; + intel.ObserveUntil(t0); + MarchData m = world.StartMarch(LocalPlayer.Id, home.id, far.id, 200, t0, out _); + world.AssignUnits(m, new[] { 200, 0, 0, 0 }); + long back = m.arriveMs + (m.arriveMs - t0); + + // Игра закрыта до половины обратного пути: войско дошло, бой, идёт домой + var events = new List(); + long now = m.arriveMs + (back - m.arriveMs) / 2; + world.AdvanceInSteps(now, events, t => intel.ObserveUntil(t)); + Assert.AreEqual(1, events.Count, "бой посчитан"); + Assert.IsTrue(m.survivors > 0); + Assert.IsTrue(world.TryGetPositionAt(m, now, out int x, out _, out _, out _, out _)); + Assert.AreNotEqual(far.x, x, "войско в пути домой, а не у цели"); + for (int i = 0; i <= 20; i++) + { + int px = home.x + (far.x - home.x) * i / 20, py = home.y + (far.y - home.y) * i / 20; + Assert.AreNotEqual(MapVisibility.Unexplored, intel.GetVisibility(px, py), $"точка пути {i}/20"); + } + } } diff --git a/Assets/Scripts/Modules/World/MarchManager.cs b/Assets/Scripts/Modules/World/MarchManager.cs index aa85ba9..a8d5070 100644 --- a/Assets/Scripts/Modules/World/MarchManager.cs +++ b/Assets/Scripts/Modules/World/MarchManager.cs @@ -101,12 +101,10 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant long now = NowMs; CatchUpPopulationBeforeBattles(now); - // Сначала выходят патрули, потом Разведка досчитывает наблюдение до "сейчас" -- пока вернувшиеся - // походы ещё на карте (после расчёта они исчезнут) - World.SpawnPatrolsUntil(now); - EventBus.Publish(new WorldAdvancingEvent { ToMs = now }); + // Мир досчитывается по событиям, перед каждым шагом Разведка досчитывает наблюдение до него: + // походы ещё на карте и их выжившие уже известны (после офлайна обратный путь тоже виден) events.Clear(); - World.Advance(now, events); + World.AdvanceInSteps(now, events, t => EventBus.Publish(new WorldAdvancingEvent { ToMs = t })); foreach (var e in events) { diff --git a/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Patrols.cs b/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Patrols.cs index cc8dad7..9c2d204 100644 --- a/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Patrols.cs +++ b/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Patrols.cs @@ -115,6 +115,38 @@ namespace Bogatyri.World }); } + // Продвинуть мир до nowMs по событиям: перед каждым шагом beforeStep(момент) -- Разведка досчитывает + // наблюдение до этого момента. Так после офлайна обратный путь войска считается уже после боя + // (выжившие известны), а не как "стоят у цели". Итог тот же, что у одного Advance(nowMs). + public void AdvanceInSteps(long nowMs, List events, Action beforeStep) + { + SpawnPatrolsUntil(nowMs); + long next; + while ((next = NextEventMs(nowMs)) >= 0) + { + beforeStep?.Invoke(next); + Advance(next, events); + } + beforeStep?.Invoke(nowMs); + Advance(nowMs, events); + } + + // Ближайшее событие мира не позже nowMs (приход, конец сбора, исчезновение точки, возвращение); -1 -- нет + public long NextEventMs(long nowMs) + { + long best = -1; + foreach (var m in marches) + { + long at = m.state == MarchState.Outbound ? m.arriveMs + : m.state == MarchState.Gathering ? m.gatherEndMs + : m.state == MarchState.Returning ? m.returnMs : long.MaxValue; + if (at <= nowMs && (best < 0 || at < best)) best = at; + } + foreach (var p in points) + if (p.active && p.occupiedBy == 0 && p.expireMs <= nowMs && (best < 0 || p.expireMs < best)) best = p.expireMs; + return best; + } + static long Dist(MapNode a, MapNode b) => WorldMapLayout.Distance(a, b); // Где войско в момент t (Unix мс) -- по времени похода, а не по текущему состоянию: diff --git a/Docs/Modules/Intelligence.md b/Docs/Modules/Intelligence.md index e287131..0770750 100644 --- a/Docs/Modules/Intelligence.md +++ b/Docs/Modules/Intelligence.md @@ -106,4 +106,5 @@ | Дата | Кто | Что | | --- | --- | --- | +| 31.10.2026 | Claude | Исправлено: после перезапуска или перемотки обратный путь войска не открывал туман (до расчёта боя Разведка считала, что войско стоит у цели). Мир досчитывается по событиям (`WorldSimulation.AdvanceInSteps`), Разведка — до каждого из них. Туман темнее. | | 31.10.2026 | Claude | Модуль создан: этап М1 — туман, источники обзора, «Обзор», чужие армии, примерная численность, опознание, последний контакт, офлайн-наблюдение, сохранение. Патрули-заглушки соседей для проверки (выключаются в `MarchConfig`). |