Feature/3d hud #18
@@ -125,6 +125,8 @@ public interface IWorld
|
||||
{
|
||||
int ActiveMarches { get; }
|
||||
bool TryStartMarch(string targetId, int soldiers, out string error);
|
||||
// Поход «Разведка» (решения 31.10, п. 2): дойти до цели и вернуться -- без боя, сбора и потерь, занимает слот похода
|
||||
bool TryStartScout(string targetId, int soldiers, out string error);
|
||||
}
|
||||
|
||||
// ---------------- Мир -> Разведка: что где находится (решение №46) ----------------
|
||||
|
||||
@@ -73,6 +73,7 @@ public struct MarchStartedEvent
|
||||
public long TimeMs; // выступление
|
||||
public string TargetId;
|
||||
public int Soldiers;
|
||||
public bool IsScout; // поход «Разведка»: без боя и добычи
|
||||
}
|
||||
|
||||
public struct MarchBattleResolvedEvent
|
||||
@@ -95,6 +96,7 @@ public struct MarchReturnedEvent
|
||||
public int Sent; // сколько воинов уходило в поход
|
||||
public bool TargetEmpty; // лагерь уже был разбит / ресурсной точки уже не было: боя и сбора не было, вернулись все
|
||||
public bool IsGather; // поход на ресурсную точку (сбор, а не бой)
|
||||
public bool IsScout; // поход «Разведка»: дошли до цели и вернулись, боя и добычи нет
|
||||
public string TargetName; // куда ходили ("Замок Змея Горыныча", имя соседа)
|
||||
public string LootText; // добыча текстом: "+20 золота, +40 еды" или "нет"
|
||||
}
|
||||
|
||||
@@ -1631,4 +1631,63 @@ public class ModuleTests
|
||||
Assert.AreEqual(2, events.Count, "два боя -- каждое войско с целью, а не друг с другом");
|
||||
Assert.AreEqual(solo.survivors, mine.survivors, "первым дошёл -- бьётся с лагерем так же, как в одиночку");
|
||||
}
|
||||
|
||||
// Решения 31.10, п. 2: поход «Разведка» -- до цели и обратно, без боя, сбора и потерь, физически, по времени
|
||||
[Test]
|
||||
public void World_ScoutMarch_NoBattle_ReturnsWholeArmy()
|
||||
{
|
||||
long t0 = 1_000_000_000_000;
|
||||
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
||||
MapNode home = world.GetNode(WorldMapLayout.PlayerCityId);
|
||||
MapNode boss = world.GetNode(WorldMapLayout.BossCastleId);
|
||||
MarchData m = world.StartMarch(LocalPlayer.Id, home.id, boss.id, 1, t0, out string error, scout: true);
|
||||
Assert.IsNotNull(m, error);
|
||||
world.AssignUnits(m, new[] { 0, 0, 1, 0 });
|
||||
Assert.IsTrue(m.scout);
|
||||
Assert.IsFalse(m.gather);
|
||||
|
||||
var events = new List<WorldEvent>();
|
||||
world.Advance(m.arriveMs, events);
|
||||
Assert.AreEqual(1, events.Count);
|
||||
Assert.AreEqual(WorldEventType.ScoutArrived, events[0].Type, "не бой");
|
||||
Assert.AreEqual(1, m.survivors, "один кавалерист против замка -- и жив: боя нет");
|
||||
Assert.AreEqual(0, m.defenderLosses);
|
||||
Assert.AreEqual(0, m.loot.Count, "без добычи");
|
||||
Assert.AreEqual(m.arriveMs + world.TravelTimeMs(boss, home, m.units), m.returnMs, "назад -- пешком, по времени");
|
||||
Assert.IsTrue(world.TryGetPositionAt(m, (m.arriveMs + m.returnMs) / 2, out _, out _, out _, out _, out _), "на обратном пути -- на карте");
|
||||
|
||||
events.Clear();
|
||||
world.Advance(m.returnMs, events);
|
||||
Assert.AreEqual(WorldEventType.MarchReturned, events[0].Type);
|
||||
Assert.AreEqual(new[] { 0, 0, 1, 0 }[2], m.survivorUnits[2], "состав не изменился");
|
||||
Assert.AreEqual(0, world.Marches.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void World_ScoutMarch_ClearedCampAndBusyPoint_DoNotBlock_AndDoNotClear()
|
||||
{
|
||||
long t0 = 1_000_000_000_000;
|
||||
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem(BattleRules.NoRandom));
|
||||
world.Advance(t0, new List<WorldEvent>());
|
||||
|
||||
// Разведка лагеря не разбивает его
|
||||
MarchData scoutCamp = world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, "camp_4", 500, t0, out _, scout: true);
|
||||
world.Advance(scoutCamp.arriveMs, new List<WorldEvent>());
|
||||
Assert.IsFalse(world.IsCampCleared("camp_4", scoutCamp.arriveMs));
|
||||
|
||||
// Точка: разведка не занимает её и не мешает сбору; к занятой точке разведку отправить можно
|
||||
ResourcePoint wood = FirstPoint(world, 0);
|
||||
long t1 = scoutCamp.arriveMs;
|
||||
MarchData look = world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, wood.id, 1, t1, out string e1, scout: true);
|
||||
Assert.IsNotNull(look, e1);
|
||||
Assert.IsTrue(world.CanGatherAt(wood.id, t1, out string e2), "разведка не занимает точку: " + e2);
|
||||
MarchData gather = world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, wood.id, 10, t1, out _);
|
||||
Assert.IsNotNull(gather);
|
||||
Assert.IsNotNull(world.StartMarch(LocalPlayer.Id, WorldMapLayout.PlayerCityId, wood.id, 1, t1, out _, scout: true), "к занятой точке -- можно");
|
||||
|
||||
var events = new List<WorldEvent>();
|
||||
world.Advance(look.arriveMs, events);
|
||||
Assert.AreEqual(wood.stock, wood.stockMax, "разведка ничего не собрала");
|
||||
Assert.IsTrue(look.scout && look.loot.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -31,8 +31,11 @@ public static class MarchReturnPopup
|
||||
showing = true;
|
||||
|
||||
MarchReturnedEvent e = queue.Dequeue();
|
||||
string target = string.IsNullOrEmpty(e.TargetName) ? "поход" : (e.IsGather ? "Сбор" : "Поход на") + $" «{e.TargetName}»";
|
||||
string result = e.IsGather
|
||||
string target = string.IsNullOrEmpty(e.TargetName) ? "поход"
|
||||
: (e.IsScout ? "Разведка" : e.IsGather ? "Сбор" : "Поход на") + $" «{e.TargetName}»";
|
||||
string result = e.IsScout
|
||||
? (e.TargetEmpty ? "цели уже не было" : "<color=#7CD67C>цель разведана</color>")
|
||||
: e.IsGather
|
||||
? (e.TargetEmpty ? "точки уже не было или её заняли, сбора не было" : "<color=#7CD67C>ресурсы собраны</color>")
|
||||
: e.TargetEmpty ? "лагерь уже был разбит, боя не было"
|
||||
: e.Victory ? "<color=#7CD67C>победа</color>" : "<color=#E06A5A>поражение</color>";
|
||||
|
||||
@@ -113,7 +113,8 @@ public static class ArmiesWindow
|
||||
switch (m.state)
|
||||
{
|
||||
case MarchState.Outbound:
|
||||
action = m.gather ? $"идёт на сбор: <b>{target}</b>" : $"идёт в бой: <b>{target}</b>";
|
||||
action = m.scout ? $"идёт в разведку: <b>{target}</b>"
|
||||
: m.gather ? $"идёт на сбор: <b>{target}</b>" : $"идёт в бой: <b>{target}</b>";
|
||||
timer = $"до цели {Clock(m.arriveMs - now)}, вернётся примерно через {Clock(ExpectedReturn(m) - now)}";
|
||||
break;
|
||||
case MarchState.Gathering:
|
||||
@@ -121,7 +122,8 @@ public static class ArmiesWindow
|
||||
timer = $"сбор ещё {Clock(m.gatherEndMs - now)}, вернётся через {Clock(ExpectedReturn(m) - now)}";
|
||||
break;
|
||||
default:
|
||||
action = (m.gather ? "возвращается со сбора: " : m.victory ? "возвращается с победой: " : "возвращается: ") + $"<b>{target}</b>";
|
||||
action = (m.scout ? "возвращается из разведки: " : m.gather ? "возвращается со сбора: "
|
||||
: m.victory ? "возвращается с победой: " : "возвращается: ") + $"<b>{target}</b>";
|
||||
timer = $"дома через {Clock(m.returnMs - now)}";
|
||||
break;
|
||||
}
|
||||
@@ -183,7 +185,7 @@ public static class ArmiesWindow
|
||||
{
|
||||
switch (m.state)
|
||||
{
|
||||
case MarchState.Outbound: return m.gather ? new Color(0.25f, 0.55f, 0.3f) : new Color(0.7f, 0.25f, 0.2f);
|
||||
case MarchState.Outbound: return m.scout ? new Color(0.45f, 0.45f, 0.55f) : m.gather ? new Color(0.25f, 0.55f, 0.3f) : new Color(0.7f, 0.25f, 0.2f);
|
||||
case MarchState.Gathering: return new Color(0.75f, 0.6f, 0.15f);
|
||||
default: return new Color(0.25f, 0.45f, 0.75f);
|
||||
}
|
||||
@@ -193,7 +195,7 @@ public static class ArmiesWindow
|
||||
{
|
||||
switch (m.state)
|
||||
{
|
||||
case MarchState.Outbound: return m.gather ? "С" : "Б"; // на сбор / в бой
|
||||
case MarchState.Outbound: return m.scout ? "Р" : m.gather ? "С" : "Б"; // разведка / на сбор / в бой
|
||||
case MarchState.Gathering: return "Д"; // добыча
|
||||
default: return "В"; // возвращается
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class MarchView : MonoBehaviour
|
||||
long secondsLeft = System.Math.Max(0, (endMs - nowMs + 999) / 1000);
|
||||
string direction = returning ? (march.cargo > 0 ? $"домой, груз {march.cargo}" : "домой")
|
||||
: gathering ? $"сбор {march.cargo}"
|
||||
: march.gather ? "на сбор" : "в бой";
|
||||
: march.scout ? "разведка" : march.gather ? "на сбор" : "в бой";
|
||||
// Марши идут часами (регламент 31.10) -- «1:05:30», меньше часа -- «12:30»
|
||||
string time = secondsLeft >= 3600
|
||||
? $"{secondsLeft / 3600}:{secondsLeft / 60 % 60:00}:{secondsLeft % 60:00}"
|
||||
|
||||
@@ -106,6 +106,7 @@ public class WorldMapController : MonoBehaviour
|
||||
if (circleNavButton != null) circleNavButton.onClick.AddListener(CircleWindow.Open); // модуль Круга
|
||||
HookUpComingSoon(settlementNavButton, "Поселение");
|
||||
sendButton.onClick.AddListener(OnSendPressed);
|
||||
CreateScoutButton();
|
||||
cancelButton.onClick.AddListener(CloseAttackPanel);
|
||||
popupOkButton.onClick.AddListener(ShowNextPopup);
|
||||
soldiersSlider.onValueChanged.AddListener(_ => { UpdateSoldiersToSendText(); if (attackPanel.activeSelf) RefreshAttackConditions(); });
|
||||
@@ -124,6 +125,7 @@ public class WorldMapController : MonoBehaviour
|
||||
if (MarchManager.Instance == null) return;
|
||||
MarchManager.Instance.OnBattleResolved += HandleBattleResolved;
|
||||
MarchManager.Instance.OnGatherStarted += HandleGatherStarted;
|
||||
MarchManager.Instance.OnScoutArrived += HandleScoutArrived;
|
||||
MarchManager.Instance.OnMarchReturned += HandleMarchReturned;
|
||||
}
|
||||
|
||||
@@ -132,6 +134,7 @@ public class WorldMapController : MonoBehaviour
|
||||
if (MarchManager.Instance == null) return;
|
||||
MarchManager.Instance.OnBattleResolved -= HandleBattleResolved;
|
||||
MarchManager.Instance.OnGatherStarted -= HandleGatherStarted;
|
||||
MarchManager.Instance.OnScoutArrived -= HandleScoutArrived;
|
||||
MarchManager.Instance.OnMarchReturned -= HandleMarchReturned;
|
||||
}
|
||||
|
||||
@@ -165,6 +168,41 @@ public class WorldMapController : MonoBehaviour
|
||||
button.onClick.AddListener(ArmiesWindow.Open);
|
||||
}
|
||||
|
||||
// Кнопка «Разведка» в окне атаки (решения 31.10, п. 2) -- копия «Отправить». Три кнопки в ряд: Отмена, Разведка, Отправить
|
||||
private Button scoutButton;
|
||||
|
||||
void CreateScoutButton()
|
||||
{
|
||||
var copy = Instantiate(sendButton.gameObject, sendButton.transform.parent);
|
||||
copy.name = "ScoutButton";
|
||||
scoutButton = copy.GetComponent<Button>();
|
||||
scoutButton.onClick.RemoveAllListeners();
|
||||
scoutButton.onClick.AddListener(OnScoutPressed);
|
||||
var label = copy.GetComponentInChildren<TMP_Text>();
|
||||
if (label != null) label.text = "Разведка";
|
||||
|
||||
Place(cancelButton, -290f);
|
||||
Place(scoutButton, 0f);
|
||||
Place(sendButton, 290f);
|
||||
}
|
||||
|
||||
static void Place(Button button, float x)
|
||||
{
|
||||
var rt = (RectTransform)button.transform;
|
||||
rt.anchoredPosition = new Vector2(x, rt.anchoredPosition.y);
|
||||
rt.sizeDelta = new Vector2(Mathf.Min(rt.sizeDelta.x, 260f), rt.sizeDelta.y);
|
||||
}
|
||||
|
||||
void OnScoutPressed()
|
||||
{
|
||||
if (selectedTarget == null) return;
|
||||
int soldiers = (int)soldiersSlider.value;
|
||||
hintText.text = MarchManager.Instance.TryStartScout(selectedTarget.id, soldiers, out string error)
|
||||
? $"Разведка выступила: {soldiers} воинов идут к цели «{selectedTarget.displayName}» и вернутся без боя"
|
||||
: "Разведка не начата: " + error;
|
||||
CloseAttackPanel();
|
||||
}
|
||||
|
||||
void HookUpComingSoon(Button button, string sectionName)
|
||||
{
|
||||
if (button == null) return;
|
||||
@@ -415,6 +453,7 @@ public class WorldMapController : MonoBehaviour
|
||||
soldiersSlider.interactable = available > 0;
|
||||
bool slotFree = !GameServices.TryGet(out IArmy slotsArmy) || slotsArmy.ActiveMarches < slotsArmy.MarchSlots;
|
||||
sendButton.interactable = available > 0 && slotFree;
|
||||
if (scoutButton != null) scoutButton.interactable = available > 0 && slotFree;
|
||||
UpdateSoldiersToSendText();
|
||||
RefreshAttackConditions(); // путь -- по составу при выбранном числе воинов
|
||||
|
||||
@@ -669,6 +708,14 @@ public class WorldMapController : MonoBehaviour
|
||||
EnqueuePopup(title, body);
|
||||
}
|
||||
|
||||
void HandleScoutArrived(MarchData march)
|
||||
{
|
||||
string target = World.TargetName(march);
|
||||
hintText.text = march.targetEmpty
|
||||
? $"Разведка: «{target}» уже нет, войско возвращается"
|
||||
: $"Разведка дошла до цели «{target}» и возвращается домой";
|
||||
}
|
||||
|
||||
void HandleGatherStarted(MarchData march)
|
||||
{
|
||||
string target = World.TargetName(march);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
| `ILeveledBuilding` | Здания с уровнем (`ResourceCollector`) | Уровень здания — его сохраняет Поселение. |
|
||||
| `IPopulation` | Население (`PopulationService`) | Жители, рабочие по профессиям, воины, предел жилья, расход еды и прирост в час, голод и его история (`WasStarvingAt`), найм воинов из жителей; `Tick` — досчитать до момента. |
|
||||
| `IArmy` | Армия (`ArmyService`) | Войска по типам, в городе и в походах; обучение, потери; забрать в поход, вернуть выживших. |
|
||||
| `IWorld` | Мир (`MarchManager`) | Сколько походов в пути; начать поход. |
|
||||
| `IWorld` | Мир (`MarchManager`) | Сколько походов в пути; начать поход; начать поход «Разведка» (`TryStartScout`). |
|
||||
| `IWorldMap` | Мир (`MarchManager`) | Для Разведки: границы карты, где поселение, все армии в момент времени (`ArmyOnMap`). |
|
||||
| `IIntelligence` | Разведка (`IntelligenceService`) | Туман (`GetVisibility`), «Обзор», известные чужие армии (`KnownContact`), `ObserveUntil`. |
|
||||
| `IBattleSystem` | Бой (`BattleSystem`) | Посчитать исход боя по `BattleRequest`: состав сторон по типам, стены, бонусы. |
|
||||
@@ -60,6 +60,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | `IWorld.TryStartScout`; `IsScout` у `MarchStartedEvent` и `MarchReturnedEvent` (решения 31.10, п. 2). |
|
||||
| 31.10.2026 | Claude | Разведка М1: контракты `IWorldMap`, `IIntelligence` (`ArmyOnMap`, `MapVisibility`, `KnownContact`), события `WorldAdvancingEvent`, `ArmySpottedEvent`, `ArmyIdentifiedEvent`, `ArmyLostContactEvent`. |
|
||||
| 31.10.2026 | Claude | `IPopulation.CompleteTraining(type, n)` — передача новобранцев в Армию одним действием. |
|
||||
| 31.10.2026 | Claude | `IPopulation.Workforce` — все жители (ограничения «40% всех людей» больше нет), `CivilianReserve` — по уровню Ратуши. Население — самостоятельный модуль. |
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | Поход «Разведка» (решения 31.10, п. 2) — команда Мира; что войско увидит, показывает Разведка. См. World.md. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 1: объекты видны всегда, а гарнизоны, запас и срок точек, прогноз боя и сбора — только в зоне обзора (`WorldMapController`). |
|
||||
| 31.10.2026 | Claude | Исправлено: после перезапуска или перемотки обратный путь войска не открывал туман (до расчёта боя Разведка считала, что войско стоит у цели). Мир досчитывается по событиям (`WorldSimulation.AdvanceInSteps`), Разведка — до каждого из них. Туман темнее. |
|
||||
| 31.10.2026 | Claude | Модуль создан: этап М1 — туман, источники обзора, «Обзор», чужие армии, примерная численность, опознание, последний контакт, офлайн-наблюдение, сохранение. Патрули-заглушки соседей для проверки (выключаются в `MarchConfig`). |
|
||||
|
||||
@@ -105,6 +105,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 2: поход «Разведка». Обновлены World, Core, Intelligence. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 6: 24 ресурсные точки. Обновлён World. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 5: разгром дальше 5:1 (92% / 95% / до 99%). Обновлён Battle. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 4: средняя скорость смешанного войска. Обновлён World. |
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
| --- | --- |
|
||||
| `ActiveMarches` | Сколько походов в пути. |
|
||||
| `TryStartMarch(targetId, soldiers, out error)` | Отправить войско на цель. |
|
||||
| `TryStartScout(targetId, soldiers, out error)` | Поход «Разведка»: дойти до цели и вернуться — без боя, сбора и потерь. |
|
||||
|
||||
`MarchManager` реализует и `IWorldMap` — для Разведки: границы карты, где поселение, где все армии в момент времени (`WorldSimulation.TryGetPositionAt`). `ActiveMarches` считает только походы игрока.
|
||||
|
||||
@@ -116,6 +117,14 @@
|
||||
|
||||
**Панель армий** (регламент 31.10, раздел 7: «нужна всем игрокам»): кнопка «Армии» на карте мира, под «Рейтингом» — окно `WorldMap/ArmiesWindow.cs`. Вверху — дома свободно, гарнизон, походов N из M. По каждому войску игрока: куда и что делает (идёт в бой / идёт на сбор / собирает / возвращается), состав по типам, груз, сколько до цели или до конца сбора и когда будет дома. Таймеры обновляются сами, пока окно открыто. До отправки окно атаки показывает длительность всего задания: для боя «туда и обратно ~X», для сбора «весь поход ~X».
|
||||
|
||||
**Поход «Разведка»** (решения по 8 вопросам Саши от 31.10, п. 2): кнопка «Разведка» в окне атаки, между «Отмена» и «Отправить».
|
||||
- Цель — любая, кроме своего города: замок, сосед, лагерь (и разбитый), ресурсная точка (и занятая).
|
||||
- Занимает обычный слот похода; воинов забирает Армия, как для боя.
|
||||
- У цели боя, сбора и потерь нет: войско поворачивает домой тем же составом (`WorldEventType.ScoutArrived`) и идёт обратно по времени, без телепортации. Цели уже нет (точка исчезла) — дошли до места и вернулись (`targetEmpty`).
|
||||
- Лагерь не разбивается, точка не занимается и не мешает чужому сбору.
|
||||
- Что войско увидит по пути и у цели — показывает Разведка (обзор армии 1 клетка): гарнизон цели, чужие армии. Отчёты и шпионаж — М2–М4.
|
||||
- Окно «ВОЙСКО ВЕРНУЛОСЬ»: «Разведка «…»: цель разведана». В событиях — `MarchStartedEvent.IsScout`, `MarchReturnedEvent.IsScout`.
|
||||
|
||||
**Армии в пути проходят друг через друга** (решения по 8 вопросам Саши от 31.10, п. 3): встреча маршрутов, даже в одной точке в один момент, — не столкновение, не остановка и не перехват. Бой — только у выбранной цели. Так же и с патрулями-заглушками. Команды «Перехватить» нет. Тест `World_ArmiesPassThroughEachOther_NoBattleOnTheWay`.
|
||||
|
||||
**Порядок событий** (документ 02: офлайн — в причинно-следственном порядке): `WorldSimulation.Advance` каждый раз берёт самое раннее событие — приход к цели (бой или начало сбора), конец сбора, исчезновение ресурсной точки или возвращение. Бой назначает время возвращения, поэтому за один вызов поход может и сразиться, и вернуться. При равном времени — бой раньше возвращения, затем по номеру похода. Раньше за один вызов сначала шли все бои, потом все возвращения — войско, вернувшееся раньше чужого боя, попадало в события после него. Повторный вызов с тем же временем ничего не создаёт заново.
|
||||
@@ -149,6 +158,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 2: поход «Разведка» (`IWorld.TryStartScout`, `MarchData.scout`, `WorldEventType.ScoutArrived`), кнопка в окне атаки, тексты в окне армий, на отряде и в окне возвращения. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 3: армии в пути проходят друг через друга — правило записано, тест на одну точку в один момент. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 6: 24 ресурсные точки (лес 8, камень 7, металл 5, золото 4; размеры 12 / 8 / 4), число по ресурсу и размеру — в `WorldResourceConfig`; новая точка — того же размера; старые сохранения дополняются. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 4: скорость смешанного войска — средняя по численности с модификаторами (вместо «по самому медленному»), без промежуточного округления. `MarchConfig`: `mixedUsesSlowest` убран, добавлены `typeSpeedBonusPercent`, `armySpeedBonusPercent`. |
|
||||
|
||||
Reference in New Issue
Block a user