Поход «Разведка» (решения 31.10, п. 2): до цели и обратно без боя, сбора и потерь, обычный слот

IWorld.TryStartScout, MarchData.scout, WorldEventType.ScoutArrived; кнопка «Разведка» в окне атаки;
разведка не разбивает лагерь и не занимает точку.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-01 13:11:55 +01:00
co-authored by Claude Opus 5.5
parent 24268f3cfb
commit 33db41002e
14 changed files with 208 additions and 17 deletions
+26 -3
View File
@@ -28,6 +28,7 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
public event Action<MarchData> OnBattleResolved;
public event Action<MarchData> OnMarchReturned;
public event Action<MarchData> OnGatherStarted; // дошли до ресурсной точки (targetEmpty -- точки уже нет)
public event Action<MarchData> OnScoutArrived; // разведка дошла до цели и повернула домой
// Время мира -- по часам игры (GameClock): перемотка "+1 час" в отладочной панели проматывает и походы.
// Миллисекунды берём от настоящих часов, чтобы отряды на карте двигались плавно. В мультиплеере -- время сервера.
@@ -111,6 +112,7 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
if (e.Type == WorldEventType.BattleResolved) HandleBattleResolved(e.March);
else if (e.Type == WorldEventType.MarchReturned) HandleMarchReturned(e.March);
else if (e.Type == WorldEventType.GatherStarted) HandleGatherStarted(e.March);
else if (e.Type == WorldEventType.ScoutArrived) HandleScoutArrived(e.March);
else if (e.Type == WorldEventType.GatherFinished)
Debug.Log($"[СБОР] Сбор окончен: {e.March.cargo} ({World.TargetName(e.March)}), войско идёт домой");
}
@@ -182,6 +184,17 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
}
public bool TryStartMarch(string targetId, int soldiers, out string error)
{
return TryStart(targetId, soldiers, false, out error);
}
// Поход «Разведка» (решения 31.10, п. 2): тот же путь отправки и тот же слот, но без боя и сбора
public bool TryStartScout(string targetId, int soldiers, out string error)
{
return TryStart(targetId, soldiers, true, out error);
}
bool TryStart(string targetId, int soldiers, bool scout, out string error)
{
if (!GameServices.TryGet(out IArmy army))
{
@@ -203,7 +216,7 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
return false;
}
MarchData march = World.StartMarch(LocalPlayerId, WorldMapLayout.PlayerCityId, targetId, soldiers, NowMs, out error);
MarchData march = World.StartMarch(LocalPlayerId, WorldMapLayout.PlayerCityId, targetId, soldiers, NowMs, out error, scout);
if (march == null) return false;
// Отправка целиком или никак: поход существует, только если Армия действительно забрала воинов.
@@ -218,9 +231,9 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
World.AssignUnits(march, army.GetMarchUnits(march.id));
if (GameServices.TryGet(out ISaveSystem save)) save.SaveNow();
Debug.Log($"[ПОХОД] {soldiers} солдат выступили на {World.GetNode(targetId).displayName}");
Debug.Log($"[ПОХОД] {soldiers} солдат выступили {(scout ? "в разведку" : "на")} {World.GetNode(targetId).displayName}");
OnMarchStarted?.Invoke(march);
EventBus.Publish(new MarchStartedEvent { MarchId = march.id, TimeMs = march.departMs, TargetId = targetId, Soldiers = soldiers });
EventBus.Publish(new MarchStartedEvent { MarchId = march.id, TimeMs = march.departMs, TargetId = targetId, Soldiers = soldiers, IsScout = scout });
return true;
}
@@ -251,6 +264,15 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
OnGatherStarted?.Invoke(march);
}
void HandleScoutArrived(MarchData march)
{
string target = World.TargetName(march);
Debug.Log(march.targetEmpty
? $"[РАЗВЕДКА] {target}: цели уже нет -- войско возвращается"
: $"[РАЗВЕДКА] Войско дошло до цели: {target}, возвращается домой");
OnScoutArrived?.Invoke(march);
}
// Солдаты и добыча зачисляются, только когда войско дошло до дома
void HandleMarchReturned(MarchData march)
{
@@ -286,6 +308,7 @@ public class MarchManager : MonoBehaviour, IWorld, IWorldMap, ISaveParticipant
Sent = march.soldiers,
TargetEmpty = march.targetEmpty,
IsGather = march.gather,
IsScout = march.scout,
TargetName = World.TargetName(march),
LootText = FormatLoot(march.loot)
});
@@ -217,7 +217,7 @@ namespace Bogatyri.World
ResourcePoint p = GetPoint(pointId);
if (p == null || !p.active || nowMs >= p.expireMs) { error = "Этой ресурсной точки больше нет"; return false; }
if (p.occupiedBy != 0) { error = "Точку уже занимает войско"; return false; }
if (marches.Exists(m => m.targetNodeId == pointId && m.state == MarchState.Outbound))
if (marches.Exists(m => m.targetNodeId == pointId && m.state == MarchState.Outbound && m.gather)) // разведка не мешает
{
error = "К этой точке уже идёт войско";
return false;
@@ -29,6 +29,7 @@ namespace Bogatyri.World
public bool targetEmpty; // лагерь уже разбит / ресурсная точка исчезла или занята: боя и сбора нет, все возвращаются
public bool gather; // поход на ресурсную точку: сбор вместо боя
public bool patrol; // патруль-заглушка соседа (Разведка М1): без боя и добычи, Армии игрока не касается
public bool scout; // поход «Разведка» (решения 31.10, п. 2): дойти до цели и вернуться, без боя и сбора
public long gatherEndMs; // когда сбор закончится (груз полон, точка истощена или исчезла)
public int cargo; // сколько ресурса несёт войско (известно с начала сбора)
public int defenders; // гарнизон цели в момент боя
@@ -143,7 +144,8 @@ namespace Bogatyri.World
BattleResolved, // войско дошло, бой прошёл
MarchReturned, // войско вернулось домой
GatherStarted, // войско дошло до ресурсной точки и начало сбор (или точка исчезла -- targetEmpty)
GatherFinished // сбор закончен, войско идёт домой с грузом
GatherFinished, // сбор закончен, войско идёт домой с грузом
ScoutArrived // разведка дошла до цели и повернула домой (targetEmpty -- цели уже нет)
}
public struct WorldEvent
@@ -253,20 +255,26 @@ namespace Bogatyri.World
// ---------------- Походы ----------------
public MarchData StartMarch(string ownerId, string fromId, string targetId, int soldiers, long nowMs, out string error)
// scout -- поход «Разведка»: к любой цели, кроме своего города; без боя, без сбора, разбитый лагерь и занятая точка не мешают
public MarchData StartMarch(string ownerId, string fromId, string targetId, int soldiers, long nowMs, out string error, bool scout = false)
{
MapNode from = GetNode(fromId);
MapNode target = GetNode(targetId);
if (from == null || target == null) { error = "Неизвестная точка на карте"; return null; }
if (target == from || !target.canBeAttacked) { error = $"На {target.displayName} нападать нельзя"; return null; }
if (target.type == MapNodeType.Camp && IsCampCleared(targetId, nowMs))
if (!scout && target.type == MapNodeType.Camp && IsCampCleared(targetId, nowMs))
{
error = $"{target.displayName} уже разбит. Появится снова через {(CampRespawnInMs(targetId, nowMs) + 59_999) / 60_000} мин";
return null;
}
if (soldiers <= 0) { error = "Нужно отправить хотя бы одного солдата"; return null; }
if (target.type == MapNodeType.ResourcePoint && !CanGatherAt(targetId, nowMs, out error)) return null;
if (target.type == MapNodeType.ResourcePoint && !scout && !CanGatherAt(targetId, nowMs, out error)) return null;
if (target.type == MapNodeType.ResourcePoint && scout)
{
ResourcePoint rp = GetPoint(targetId);
if (rp == null || !rp.active || nowMs >= rp.expireMs) { error = "Этой ресурсной точки больше нет"; return null; }
}
var march = new MarchData
{
@@ -279,7 +287,8 @@ namespace Bogatyri.World
departMs = nowMs,
arriveMs = nowMs + TravelTimeMs(from, target, null), // состав уточнит AssignUnits
state = MarchState.Outbound,
gather = target.type == MapNodeType.ResourcePoint,
gather = !scout && target.type == MapNodeType.ResourcePoint,
scout = scout,
targetName = target.displayName
};
// Номер боя -- из данных похода: тот же поход всегда даёт тот же бой.
@@ -354,6 +363,20 @@ namespace Bogatyri.World
// Патруль: дошёл -- повернул обратно, вернулся -- исчез. Событий для игрока нет
next.state = nextKind == 0 ? MarchState.Returning : MarchState.Finished;
}
else if (next.scout)
{
// Разведка: дошла -- повернула домой (Разведка видит всё вокруг войска по пути и у цели), вернулась -- дома
if (nextKind == 0)
{
ArriveScout(next);
events.Add(new WorldEvent { Type = WorldEventType.ScoutArrived, March = next, TimeMs = nextMs });
}
else
{
next.state = MarchState.Finished;
events.Add(new WorldEvent { Type = WorldEventType.MarchReturned, March = next, TimeMs = nextMs });
}
}
else if (nextKind == 0 && next.gather)
{
StartGather(next);
@@ -445,6 +468,23 @@ namespace Bogatyri.World
}
}
// Разведка у цели: боя, сбора и потерь нет -- все идут домой тем же составом. Цели уже нет (точка исчезла) --
// дошли до места и вернулись физически, без телепортации (решения 31.10, п. 2)
void ArriveScout(MarchData m)
{
MapNode from = GetNode(m.fromNodeId);
MapNode target = GetNode(m.targetNodeId);
int[] units = HasUnits(m) ? m.units : null;
m.returnMs = m.arriveMs + TravelTimeMs(target, from, units);
m.state = MarchState.Returning;
m.victory = false;
m.survivors = m.soldiers;
m.survivorUnits = units != null ? (int[])units.Clone() : null;
m.loot = new List<RewardEntry>();
ResourcePoint p = target.type == MapNodeType.ResourcePoint ? GetPoint(target.id) : null;
m.targetEmpty = target.type == MapNodeType.ResourcePoint && (p == null || !p.active);
}
// Название цели похода: запомненное при выступлении (старые походы -- с карты)
public string TargetName(MarchData m)
{