Поход «Разведка» (решения 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
@@ -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)
{