Разведка М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
+82 -20
View File
@@ -90,6 +90,7 @@ public class WorldMapController : MonoBehaviour
private string attackInfoBase;
private BattleCondition shownConditions;
private bool shownObserved; // цель была в зоне обзора, когда окно атаки открылось
private ReconReport? shownReport; // цель вне обзора, но есть отчёт разведки -- гарнизон и прогноз по нему (на момент отчёта)
WorldSimulation World => MarchManager.Instance.World;
@@ -129,6 +130,7 @@ public class WorldMapController : MonoBehaviour
MarchManager.Instance.OnBattleResolved += HandleBattleResolved;
MarchManager.Instance.OnGatherStarted += HandleGatherStarted;
MarchManager.Instance.OnScoutArrived += HandleScoutArrived;
MarchManager.Instance.OnReconCompleted += HandleReconCompleted;
MarchManager.Instance.OnMarchReturned += HandleMarchReturned;
}
@@ -138,6 +140,7 @@ public class WorldMapController : MonoBehaviour
MarchManager.Instance.OnBattleResolved -= HandleBattleResolved;
MarchManager.Instance.OnGatherStarted -= HandleGatherStarted;
MarchManager.Instance.OnScoutArrived -= HandleScoutArrived;
MarchManager.Instance.OnReconCompleted -= HandleReconCompleted;
MarchManager.Instance.OnMarchReturned -= HandleMarchReturned;
}
@@ -169,39 +172,68 @@ public class WorldMapController : MonoBehaviour
var button = copy.GetComponent<Button>();
button.onClick.RemoveAllListeners();
button.onClick.AddListener(ArmiesWindow.Open);
// «Отчёты» -- под «Армиями»: последние отчёты разведки (Разведка М2)
var reports = Instantiate(ratingButton.gameObject, ratingButton.transform.parent);
reports.name = "ReportsButton";
var rrt = (RectTransform)reports.transform;
rrt.anchoredPosition = src.anchoredPosition - new Vector2(0, (src.rect.height + 12) * 2);
var rl = reports.GetComponentInChildren<TMP_Text>();
if (rl != null) rl.text = "Отчёты";
var rb = reports.GetComponent<Button>();
rb.onClick.RemoveAllListeners();
rb.onClick.AddListener(ReconReportText.OpenWindow);
}
// Кнопка «Разведка» в окне атаки (решения 31.10, п. 2) -- копия «Отправить». Три кнопки в ряд: Отмена, Разведка, Отправить
private Button scoutButton;
// Кнопки разведки в окне атаки -- копии «Отправить». Четыре в ряд: Отмена, Разведка II, Разведка III, Отправить.
// Разведка II / III (Разведка М2): наблюдать цель и принести отчёт; у ресурсной точки -- просто «Разведка» (дойти и вернуться)
private Button scoutButton, scout3Button;
private TMP_Text scoutLabel;
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 = "Разведка";
scoutButton = CopyOfSend("ScoutButton", "Разведка II", () => OnScoutPressed(2));
scoutLabel = scoutButton.GetComponentInChildren<TMP_Text>();
scout3Button = CopyOfSend("Scout3Button", "Разведка III", () => OnScoutPressed(3));
Place(cancelButton, -290f);
Place(scoutButton, 0f);
Place(sendButton, 290f);
Place(cancelButton, -330f);
Place(scoutButton, -110f);
Place(scout3Button, 110f);
Place(sendButton, 330f);
}
Button CopyOfSend(string name, string text, UnityEngine.Events.UnityAction onClick)
{
var copy = Instantiate(sendButton.gameObject, sendButton.transform.parent);
copy.name = name;
var b = copy.GetComponent<Button>();
b.onClick.RemoveAllListeners();
b.onClick.AddListener(onClick);
var label = copy.GetComponentInChildren<TMP_Text>();
if (label != null) label.text = text;
return b;
}
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);
rt.sizeDelta = new Vector2(Mathf.Min(rt.sizeDelta.x, 205f), rt.sizeDelta.y);
}
void OnScoutPressed()
void OnScoutPressed(int level)
{
if (selectedTarget == null) return;
int soldiers = (int)soldiersSlider.value;
hintText.text = MarchManager.Instance.TryStartScout(selectedTarget.id, soldiers, out string error)
? $"Разведка выступила: {soldiers} воинов идут к цели «{selectedTarget.displayName}» и вернутся без боя"
bool point = selectedTarget.type == MapNodeType.ResourcePoint;
string error;
bool ok = point
? MarchManager.Instance.TryStartScout(selectedTarget.id, soldiers, out error)
: MarchManager.Instance.TryStartRecon(selectedTarget.id, soldiers, level, out error);
long hold = !point && GameServices.TryGet(out IIntelligence intel) ? intel.ReconDurationMs(selectedTarget.id, level) : 0;
hintText.text = ok
? point ? $"Разведка выступила: {soldiers} воинов идут к цели «{selectedTarget.displayName}» и вернутся без боя"
: $"Разведка {(level == 3 ? "III" : "II")} выступила к «{selectedTarget.displayName}»: наблюдать {Duration(hold)}, потом отчёт и домой"
: "Разведка не начата: " + error;
CloseAttackPanel();
}
@@ -518,10 +550,14 @@ public class WorldMapController : MonoBehaviour
attackInfoText.fontSizeMin = 18;
attackInfoText.fontSizeMax = 32;
shownObserved = IsObserved(target);
shownReport = !shownObserved && GameServices.TryGet(out IIntelligence intelReports) && intelReports.TryGetLatestReport(target.id, out var report)
? report : (ReconReport?)null;
attackInfoBase = target.type == MapNodeType.ResourcePoint ? PointInfo(target)
: (shownObserved
? $"Гарнизон: {target.garrison} ({UnitsText(WorldMapLayout.GarrisonUnits(target))})"
: "Гарнизон неизвестен: цель вне зоны обзора") +
: shownReport.HasValue
? $"Гарнизон по отчёту разведки {Duration(MarchManager.NowMs - shownReport.Value.TimeMs)} назад: {Sum(shownReport.Value.Units)} ({UnitsText(shownReport.Value.Units)})"
: "Гарнизон неизвестен: цель вне зоны обзора. Отправьте разведку") +
(WorldMapLayout.IsFortified(target) ? ", защитники за стенами\n" : ", бой в поле\n") +
$"Награда за победу: {MarchManager.FormatLoot(target.rewards)}";
RefreshAttackConditions();
@@ -533,7 +569,14 @@ public class WorldMapController : MonoBehaviour
soldiersSlider.interactable = available > 0;
bool slotFree = !GameServices.TryGet(out IArmy slotsArmy) || slotsArmy.ActiveMarches < slotsArmy.MarchSlots;
sendButton.interactable = available > 0 && slotFree;
bool pointTarget = target.type == MapNodeType.ResourcePoint;
if (scoutButton != null) scoutButton.interactable = available > 0 && slotFree;
if (scoutLabel != null) scoutLabel.text = pointTarget ? "Разведка" : "Разведка II";
if (scout3Button != null)
{
scout3Button.gameObject.SetActive(!pointTarget); // у точки отчёта нет -- только дойти и вернуться
scout3Button.interactable = available > 0 && slotFree;
}
UpdateSoldiersToSendText();
RefreshAttackConditions(); // путь -- по составу при выбранном числе воинов
@@ -642,14 +685,15 @@ public class WorldMapController : MonoBehaviour
if (selectedTarget.type == MapNodeType.ResourcePoint) return GatherPreview(soldiers);
int[] units = PreviewUnits(soldiers); // гарнизон в поход не идёт
if (units == null) return "";
if (!shownObserved) return "прогноз боя недоступен: гарнизон цели неизвестен";
if (!shownObserved && !shownReport.HasValue) return "прогноз боя недоступен: гарнизон цели неизвестен";
int[] defenders = shownObserved ? WorldMapLayout.GarrisonUnits(selectedTarget) : shownReport.Value.Units; // по отчёту -- на момент отчёта
// Прогноз с учётом случайности ±5%: показывает риск, а не итог конкретного боя (план от 27.10.2026)
BattleForecast f = GameServices.Get<IBattleSystem>().Forecast(new BattleRequest
{
AttackerUnits = units,
DefenderUnits = WorldMapLayout.GarrisonUnits(selectedTarget),
Defenders = selectedTarget.garrison,
DefenderUnits = defenders,
Defenders = Sum(defenders),
DefenderFortified = WorldMapLayout.IsFortified(selectedTarget),
AttackerConditions = MarchManager.PlayerConditions() // голод сейчас; в бою проверится ещё раз
});
@@ -661,6 +705,13 @@ public class WorldMapController : MonoBehaviour
$"{chance}, шанс победы {ChanceText(f.WinChancePermille)}, потери {Range(f.AttackerLossesMin * 100L, f.AttackerLossesMax * 100L)}";
}
static int Sum(int[] units)
{
int n = 0;
if (units != null) foreach (var u in units) n += u;
return n;
}
// 273 десятых процента -> "27%"; около краёв -- с десятыми, чтобы 99,6% не выглядело как 100%
static string ChanceText(int permille)
{
@@ -788,8 +839,19 @@ public class WorldMapController : MonoBehaviour
EnqueuePopup(title, body);
}
void HandleReconCompleted(MarchData march)
{
if (GameServices.TryGet(out IIntelligence intel) && intel.TryGetLatestReport(march.targetNodeId, out var report))
EnqueuePopup("ОТЧЁТ РАЗВЕДКИ", ReconReportText.Format(report, MarchManager.NowMs) + "\nРазведчики возвращаются домой");
}
void HandleScoutArrived(MarchData march)
{
if (march.reconLevel > 0 && !march.targetEmpty)
{
hintText.text = $"Разведка у цели «{World.TargetName(march)}»: наблюдает {Duration(march.gatherEndMs - march.arriveMs)}";
return;
}
string target = World.TargetName(march);
hintText.text = march.targetEmpty
? $"Разведка: «{target}» уже нет, войско возвращается"