384 lines
20 KiB
C#
384 lines
20 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
|
||
// ГЛОБАЛЬНАЯ КАРТА (панель "Мир").
|
||
// Слева/сверху -- карта: ваш город, два тестовых поселения, маршруты, армии в пути с таймерами.
|
||
// Справа/снизу -- информация: армия в городе, выбранное поселение, походы, отчёты о боях,
|
||
// и окно отправки войск (сколько каких воинов, время в пути, шанс победы).
|
||
public class WorldMapPanel : BasePanel
|
||
{
|
||
string selectedId = "bandits";
|
||
bool sendMode;
|
||
readonly int[] sendUnits = new int[4];
|
||
|
||
// то, что обновляется каждый кадр без перерисовки
|
||
class MarchView { public WorldMarch m; public RectTransform marker; public TextMeshProUGUI label; public TextMeshProUGUI timer; }
|
||
readonly List<MarchView> marchViews = new List<MarchView>();
|
||
readonly List<KeyValuePair<WorldMarch, TextMeshProUGUI>> listTimers = new List<KeyValuePair<WorldMarch, TextMeshProUGUI>>();
|
||
readonly Dictionary<string, TextMeshProUGUI> ruinTimers = new Dictionary<string, TextMeshProUGUI>();
|
||
readonly TextMeshProUGUI[] sendCounts = new TextMeshProUGUI[4];
|
||
TextMeshProUGUI sendSummary;
|
||
Button sendButton;
|
||
|
||
protected override void OnEnable()
|
||
{
|
||
base.OnEnable();
|
||
sendMode = false;
|
||
}
|
||
|
||
protected override void Build(RectTransform root)
|
||
{
|
||
marchViews.Clear(); listTimers.Clear(); ruinTimers.Clear(); sendSummary = null; sendButton = null;
|
||
for (int i = 0; i < 4; i++) sendCounts[i] = null;
|
||
|
||
bool wide = root.rect.width > root.rect.height * 1.2f;
|
||
RectTransform layout;
|
||
if (wide)
|
||
{
|
||
layout = UIKit.Stretch(UIKit.Row(root, -1, 16));
|
||
}
|
||
else
|
||
{
|
||
layout = UIKit.Stretch(UIKit.Card(root, new Color(0, 0, 0, 0), 0, 12));
|
||
}
|
||
|
||
// --- карта ---
|
||
var mapHolder = UIKit.Box(layout, new Color(0.16f, 0.27f, 0.17f));
|
||
var mapLe = UIKit.Size(mapHolder, -1, -1, 1);
|
||
mapLe.flexibleHeight = wide ? 1 : 1.3f;
|
||
BuildMap((RectTransform)mapHolder.transform);
|
||
|
||
// --- боковая колонка ---
|
||
var side = UIKit.Make("Side", layout);
|
||
var sideLe = UIKit.Size(side, wide ? 640 : -1, -1);
|
||
sideLe.flexibleHeight = 1;
|
||
if (!wide) sideLe.flexibleWidth = 1;
|
||
var list = ScrollList(side, 12);
|
||
|
||
if (sendMode) BuildSend(list); else BuildInfo(list);
|
||
Tick();
|
||
}
|
||
|
||
// ===================== КАРТА =====================
|
||
|
||
void BuildMap(RectTransform map)
|
||
{
|
||
map.gameObject.AddComponent<RectMask2D>();
|
||
|
||
// декорации: "леса", "горы", "озеро" -- просто пятна, чтобы карта не была пустой
|
||
var rnd = new System.Random(7);
|
||
for (int i = 0; i < 26; i++)
|
||
{
|
||
float x = (float)rnd.NextDouble(), y = (float)rnd.NextDouble();
|
||
int kind = rnd.Next(3);
|
||
Color c = kind == 0 ? new Color(0.11f, 0.21f, 0.12f) : (kind == 1 ? new Color(0.35f, 0.35f, 0.33f) : new Color(0.2f, 0.32f, 0.45f));
|
||
float s = 30 + (float)rnd.NextDouble() * 60;
|
||
var blob = UIKit.Box(map, c);
|
||
blob.raycastTarget = false;
|
||
var brt = blob.rectTransform;
|
||
brt.anchorMin = brt.anchorMax = new Vector2(x, y);
|
||
brt.sizeDelta = new Vector2(s, s * 0.7f);
|
||
brt.localRotation = Quaternion.Euler(0, 0, rnd.Next(0, 90));
|
||
}
|
||
// сетка
|
||
for (int i = 1; i < 5; i++)
|
||
{
|
||
Line(map, new Vector2(i / 5f, 0), new Vector2(i / 5f, 1), new Color(1, 1, 1, 0.05f), 2);
|
||
Line(map, new Vector2(0, i / 5f), new Vector2(1, i / 5f), new Color(1, 1, 1, 0.05f), 2);
|
||
}
|
||
|
||
var home = WorldData.Home;
|
||
// постоянные дороги от города к поселениям
|
||
foreach (var s in WorldData.Settlements)
|
||
if (!s.isHome) Line(map, home.pos, s.pos, new Color(0.85f, 0.75f, 0.5f, 0.25f), 5);
|
||
|
||
// активные маршруты -- ярче
|
||
foreach (var m in GP.Data.marches)
|
||
{
|
||
var target = WorldData.Get(m.targetId);
|
||
if (target == null) continue;
|
||
Line(map, home.pos, target.pos, m.returning ? new Color(0.4f, 0.9f, 0.5f, 0.8f) : new Color(1f, 0.55f, 0.2f, 0.85f), 8);
|
||
}
|
||
|
||
// поселения
|
||
foreach (var s in WorldData.Settlements) SettlementIcon(map, s);
|
||
|
||
// армии в пути
|
||
foreach (var m in GP.Data.marches)
|
||
{
|
||
var v = new MarchView { m = m };
|
||
var marker = UIKit.Box(map, m.returning ? new Color(0.2f, 0.7f, 0.3f) : new Color(0.95f, 0.5f, 0.15f));
|
||
marker.raycastTarget = false;
|
||
v.marker = marker.rectTransform;
|
||
v.marker.sizeDelta = new Vector2(64, 64);
|
||
v.label = UIKit.Text(v.marker, WorldSystem.Count(m.returning ? m.survivors : m.units).ToString(), 26, Color.white, TextAlignmentOptions.Center);
|
||
UIKit.Stretch(v.label.rectTransform);
|
||
v.label.fontStyle = FontStyles.Bold;
|
||
v.timer = UIKit.Text(v.marker, "", 24, Color.white, TextAlignmentOptions.Center);
|
||
var trt = v.timer.rectTransform;
|
||
trt.anchorMin = new Vector2(0.5f, 1); trt.anchorMax = new Vector2(0.5f, 1);
|
||
trt.pivot = new Vector2(0.5f, 0); trt.sizeDelta = new Vector2(220, 34); trt.anchoredPosition = new Vector2(0, 4);
|
||
v.timer.outlineWidth = 0.25f; v.timer.outlineColor = Color.black;
|
||
marchViews.Add(v);
|
||
}
|
||
Frame();
|
||
}
|
||
|
||
void SettlementIcon(RectTransform map, SettlementDef s)
|
||
{
|
||
var st = s.isHome ? null : WorldSystem.State(s.id);
|
||
bool ruined = !s.isHome && WorldSystem.IsRuined(s.id, System.DateTime.UtcNow.Ticks);
|
||
bool selected = s.id == selectedId;
|
||
|
||
float size = s.isHome ? 130 : 110;
|
||
if (selected)
|
||
{
|
||
// золотая рамка выбранного поселения (под иконкой)
|
||
var ring = UIKit.Box(map, UIKit.Theme.Gold);
|
||
ring.raycastTarget = false;
|
||
ring.rectTransform.anchorMin = ring.rectTransform.anchorMax = s.pos;
|
||
ring.rectTransform.sizeDelta = new Vector2(size + 16, size + 16);
|
||
}
|
||
var holder = UIKit.Make("Settlement_" + s.id, map);
|
||
holder.anchorMin = holder.anchorMax = s.pos;
|
||
holder.sizeDelta = new Vector2(size, size);
|
||
var img = holder.gameObject.AddComponent<Image>();
|
||
img.color = ruined ? new Color(0.3f, 0.3f, 0.3f) : s.color;
|
||
var btn = holder.gameObject.AddComponent<Button>();
|
||
string id = s.id;
|
||
btn.onClick.AddListener(() => { selectedId = id; sendMode = false; RequestRebuild(); });
|
||
|
||
var letter = UIKit.Text(holder, s.isHome ? "ДОМ" : (ruined ? "X" : st.level.ToString()), s.isHome ? 32 : 44, Color.white, TextAlignmentOptions.Center);
|
||
UIKit.Stretch(letter.rectTransform);
|
||
letter.fontStyle = FontStyles.Bold;
|
||
|
||
var name = UIKit.Text(holder, s.name, 24, Color.white, TextAlignmentOptions.Center);
|
||
var nrt = name.rectTransform;
|
||
nrt.anchorMin = new Vector2(0.5f, 0); nrt.anchorMax = new Vector2(0.5f, 0);
|
||
nrt.pivot = new Vector2(0.5f, 1); nrt.sizeDelta = new Vector2(300, 60); nrt.anchoredPosition = new Vector2(0, -6);
|
||
name.outlineWidth = 0.25f; name.outlineColor = Color.black;
|
||
if (s.isHome) name.text = s.name + "\nсила армии " + GP.ArmyPower.ToString("N0");
|
||
else if (ruined)
|
||
{
|
||
name.text = s.name + "\nразграблено";
|
||
ruinTimers[s.id] = name;
|
||
}
|
||
else name.text = s.name + "\nзащита " + WorldSystem.GarrisonPower(st.garrison).ToString("N0");
|
||
}
|
||
|
||
WorldLine Line(RectTransform parent, Vector2 a, Vector2 b, Color c, float thickness)
|
||
{
|
||
var img = UIKit.Box(parent, c);
|
||
img.raycastTarget = false;
|
||
var l = img.gameObject.AddComponent<WorldLine>();
|
||
l.from = a; l.to = b; l.thickness = thickness;
|
||
return l;
|
||
}
|
||
|
||
// ===================== ИНФОРМАЦИЯ =====================
|
||
|
||
void BuildInfo(RectTransform list)
|
||
{
|
||
var d = GP.Data;
|
||
|
||
// --- большая карта мира (сцена WorldMap от Саши: соседи, босс, чат, рейтинг) ---
|
||
var big = UIKit.Card(list, new Color(0.16f, 0.22f, 0.3f));
|
||
UIKit.Text(big, "Большая карта: соседи, Замок Змея Горыныча, чат и рейтинг. Армия та же -- ваши воины из казарм.", 24, UIKit.Theme.Text);
|
||
UIKit.Button(big, "ОТКРЫТЬ БОЛЬШУЮ КАРТУ", UIKit.Theme.Blue, () => SceneNavigator.OpenWorldMap(), -1, 96, 32);
|
||
|
||
// --- армия в городе ---
|
||
var army = UIKit.Card(list, UIKit.Theme.CardDark);
|
||
ProfilePanel.Header(army, "Армия в городе");
|
||
UIKit.Text(army, WorldSystem.UnitsText(d.troops), 26);
|
||
UIKit.Text(army, "Воинов: " + GP.TotalTroops + " • сила " + GP.ArmyPower.ToString("N0") +
|
||
" • в походе: " + WorldSystem.TroopsInMarches, 24, UIKit.Theme.TextDim);
|
||
var ar = UIKit.Row(army, 76, 10);
|
||
UIKit.Size(UIKit.Button(ar, "Нанять войска", UIKit.Theme.Blue, () => OpenPanel("Troops"), -1, 76, 26), -1, 76, 1);
|
||
UIKit.Size(UIKit.Button(ar, "Улучшить героя", UIKit.Theme.Gray, () => OpenPanel("Hero"), -1, 76, 26), -1, 76, 1);
|
||
|
||
// --- выбранное поселение ---
|
||
var def = WorldData.Get(selectedId) ?? WorldData.Settlements[1];
|
||
var card = UIKit.Card(list, UIKit.Theme.Card);
|
||
var title = UIKit.Text(card, def.name, 34, UIKit.Theme.Gold);
|
||
title.fontStyle = FontStyles.Bold;
|
||
|
||
if (def.isHome)
|
||
{
|
||
UIKit.Text(card, "Отсюда выходят армии и сюда приносят добычу. Нажмите на поселение на карте, чтобы его атаковать.", 26, UIKit.Theme.TextDim);
|
||
UIKit.Button(card, "Вернуться к городу", UIKit.Theme.Gray, () => { if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll(); }, -1, 80);
|
||
}
|
||
else
|
||
{
|
||
var st = WorldSystem.State(def.id);
|
||
bool ruined = WorldSystem.IsRuined(def.id, System.DateTime.UtcNow.Ticks);
|
||
ProfilePanel.KV(card, "Уровень", st.level.ToString());
|
||
ProfilePanel.KV(card, "Расстояние", "~" + GameData.FormatTime(WorldSystem.TravelSeconds(def, new[] { 1, 0, 0, 0 })) + " пешком");
|
||
if (ruined)
|
||
{
|
||
var t = UIKit.Text(card, "", 28, UIKit.Theme.Red);
|
||
ruinTimers["card_" + def.id] = t;
|
||
}
|
||
else
|
||
{
|
||
ProfilePanel.KV(card, "Защитники", WorldSystem.UnitsText(st.garrison));
|
||
ProfilePanel.KV(card, "Сила защиты", WorldSystem.GarrisonPower(st.garrison).ToString("N0"));
|
||
ProfilePanel.KV(card, "Добыча", GameData.RewardsText(WorldSystem.FullLoot(def, st.level)));
|
||
}
|
||
string why = ruined ? "Отстраивается" : (d.marches.Count >= WorldData.MaxMarches ? "Все отряды в походе" : (GP.TotalTroops == 0 ? "Нет войск в городе" : null));
|
||
var b = UIKit.Button(card, why ?? "АТАКОВАТЬ", why == null ? UIKit.Theme.Red : UIKit.Theme.Gray, () =>
|
||
{
|
||
sendMode = true;
|
||
for (int i = 0; i < 4; i++) sendUnits[i] = GP.Data.troops[i]; // по умолчанию -- все
|
||
RequestRebuild();
|
||
}, -1, 96, 34);
|
||
b.interactable = why == null;
|
||
if (GP.TotalTroops == 0)
|
||
UIKit.Button(card, "Нанять войска", UIKit.Theme.Blue, () => OpenPanel("Troops"), -1, 80);
|
||
}
|
||
|
||
// --- походы ---
|
||
var marches = UIKit.Card(list, UIKit.Theme.Card);
|
||
ProfilePanel.Header(marches, "Походы (" + d.marches.Count + " / " + WorldData.MaxMarches + ")");
|
||
if (d.marches.Count == 0) UIKit.Text(marches, "Армии в городе. Выберите цель на карте.", 24, UIKit.Theme.TextDim);
|
||
foreach (var m in d.marches)
|
||
{
|
||
var mm = m;
|
||
var target = WorldData.Get(m.targetId);
|
||
var row = UIKit.Row(marches, 90, 10);
|
||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 2);
|
||
UIKit.Size(info, -1, -1, 1);
|
||
UIKit.Text(info, (m.returning ? "Домой из: " : "К цели: ") + target.name, 26).fontStyle = FontStyles.Bold;
|
||
var timer = UIKit.Text(info, "", 24, m.returning ? UIKit.Theme.Green : UIKit.Theme.Gold);
|
||
listTimers.Add(new KeyValuePair<WorldMarch, TextMeshProUGUI>(m, timer));
|
||
if (!m.returning)
|
||
UIKit.Button(row, "Отозвать", UIKit.Theme.Gray, () => WorldSystem.Recall(mm), 170, 70, 24);
|
||
}
|
||
|
||
// --- отчёты ---
|
||
var reports = UIKit.Card(list, UIKit.Theme.CardDark);
|
||
ProfilePanel.Header(reports, "Отчёты о боях");
|
||
if (d.reports.Count == 0) UIKit.Text(reports, "Пока не было сражений.", 24, UIKit.Theme.TextDim);
|
||
for (int i = 0; i < d.reports.Count && i < 5; i++)
|
||
{
|
||
var r = d.reports[i];
|
||
UIKit.Button(reports, r.title + " " + new System.DateTime(r.ticks).ToString("HH:mm"),
|
||
r.win ? new Color(0.2f, 0.4f, 0.25f) : new Color(0.45f, 0.2f, 0.2f), () => UIKit.Message(r.title, r.text), -1, 70, 24);
|
||
}
|
||
|
||
// --- подсказка цикла ---
|
||
UIKit.Text(list, "Цикл: стройте здания -> собирайте ресурсы -> нанимайте войска -> отправляйте армию -> забирайте добычу -> развивайте город.",
|
||
22, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||
}
|
||
|
||
// ===================== ОТПРАВКА =====================
|
||
|
||
void BuildSend(RectTransform list)
|
||
{
|
||
var def = WorldData.Get(selectedId);
|
||
var card = UIKit.Card(list, UIKit.Theme.Card);
|
||
UIKit.Text(card, "Поход: " + def.name, 34, UIKit.Theme.Gold).fontStyle = FontStyles.Bold;
|
||
UIKit.Text(card, "Выберите, кого отправить. Армия идёт со скоростью самого медленного воина.", 22, UIKit.Theme.TextDim);
|
||
|
||
for (int i = 0; i < 4; i++)
|
||
{
|
||
int idx = i;
|
||
var t = GameData.Troops[i];
|
||
var row = UIKit.Row(card, 70, 6);
|
||
var n = UIKit.Text(row, t.name + "\n<size=20><color=#b0b0b0>в городе " + GP.Data.troops[i] + "</color></size>", 26);
|
||
UIKit.Size(n, -1, -1, 1);
|
||
Step(row, "-10", idx, -10); Step(row, "-1", idx, -1);
|
||
sendCounts[i] = UIKit.Text(row, sendUnits[i].ToString(), 30, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||
UIKit.Size(sendCounts[i], 80);
|
||
Step(row, "+1", idx, 1); Step(row, "+10", idx, 10);
|
||
}
|
||
|
||
var quick = UIKit.Row(card, 70, 10);
|
||
UIKit.Size(UIKit.Button(quick, "Все войска", UIKit.Theme.Gray, () => { for (int i = 0; i < 4; i++) sendUnits[i] = GP.Data.troops[i]; RefreshSend(); }, -1, 70, 24), -1, 70, 1);
|
||
UIKit.Size(UIKit.Button(quick, "Сбросить", UIKit.Theme.Gray, () => { for (int i = 0; i < 4; i++) sendUnits[i] = 0; RefreshSend(); }, -1, 70, 24), -1, 70, 1);
|
||
|
||
sendSummary = UIKit.Text(card, "", 26);
|
||
|
||
var btns = UIKit.Row(card, 100, 12);
|
||
UIKit.Size(UIKit.Button(btns, "Назад", UIKit.Theme.Gray, () => { sendMode = false; RequestRebuild(); }, -1, 100, 30), -1, 100, 1);
|
||
sendButton = UIKit.Button(btns, "ОТПРАВИТЬ", UIKit.Theme.Red, () =>
|
||
{
|
||
var m = WorldSystem.Send(selectedId, (int[])sendUnits.Clone());
|
||
if (m != null) { sendMode = false; RequestRebuild(); }
|
||
}, -1, 100, 34);
|
||
UIKit.Size(sendButton, -1, 100, 1.5f);
|
||
|
||
RefreshSend();
|
||
}
|
||
|
||
void Step(Transform row, string label, int idx, int delta)
|
||
{
|
||
UIKit.Button(row, label, UIKit.Theme.CardDark, () =>
|
||
{
|
||
sendUnits[idx] = Mathf.Clamp(sendUnits[idx] + delta, 0, GP.Data.troops[idx]);
|
||
RefreshSend();
|
||
}, 74, 64, 24);
|
||
}
|
||
|
||
void RefreshSend()
|
||
{
|
||
for (int i = 0; i < 4; i++)
|
||
{
|
||
sendUnits[i] = Mathf.Clamp(sendUnits[i], 0, GP.Data.troops[i]);
|
||
if (sendCounts[i] != null) sendCounts[i].text = sendUnits[i].ToString();
|
||
}
|
||
if (sendSummary == null) return;
|
||
var def = WorldData.Get(selectedId);
|
||
var st = WorldSystem.State(selectedId);
|
||
int count = WorldSystem.Count(sendUnits);
|
||
float chance = WorldSystem.EstimateWinChance(sendUnits, selectedId);
|
||
string cc = chance >= 0.7f ? "#7ee08a" : (chance >= 0.4f ? "#f2c14e" : "#ff7a7a");
|
||
sendSummary.text =
|
||
"Воинов: " + count + " • сила " + Mathf.RoundToInt(WorldSystem.Power(sendUnits) * (1 + GP.ArmyBonus)).ToString("N0") +
|
||
" против " + WorldSystem.GarrisonPower(st.garrison).ToString("N0") + "\n" +
|
||
"Унесут добычи: " + WorldSystem.Carry(sendUnits).ToString("N0") + "\n" +
|
||
"В пути: " + (count > 0 ? GameData.FormatTime(WorldSystem.TravelSeconds(def, sendUnits)) + " туда и столько же обратно" : "-") + "\n" +
|
||
"Шанс победы: <color=" + cc + ">" + Mathf.RoundToInt(chance * 100) + "%</color>";
|
||
string why = WorldSystem.CanSendReason(selectedId, sendUnits);
|
||
if (sendButton != null) UIKit.SetButton(sendButton, why == null ? "ОТПРАВИТЬ" : why, why == null);
|
||
}
|
||
|
||
// ===================== ОБНОВЛЕНИЕ =====================
|
||
|
||
protected override void Frame()
|
||
{
|
||
var home = WorldData.Home.pos;
|
||
foreach (var v in marchViews)
|
||
{
|
||
if (v.marker == null) continue;
|
||
var target = WorldData.Get(v.m.targetId);
|
||
float p = WorldSystem.Progress(v.m);
|
||
v.marker.anchorMin = v.marker.anchorMax = Vector2.Lerp(home, target.pos, p);
|
||
v.timer.text = (v.m.returning ? "домой " : "бой через ") + GameData.FormatTime(WorldSystem.SecondsLeft(v.m));
|
||
}
|
||
}
|
||
|
||
protected override void Tick()
|
||
{
|
||
foreach (var kv in listTimers)
|
||
{
|
||
if (kv.Value == null) continue;
|
||
var m = kv.Key;
|
||
int n = WorldSystem.Count(m.returning ? m.survivors : m.units);
|
||
kv.Value.text = n + " воинов • " + (m.returning ? "вернутся через " : "прибытие через ") + GameData.FormatTime(WorldSystem.SecondsLeft(m)) +
|
||
(m.returning && m.loot.Count > 0 ? " • несут: " + GameData.RewardsText(m.loot.ToArray()) : "");
|
||
}
|
||
foreach (var kv in ruinTimers)
|
||
{
|
||
if (kv.Value == null) continue;
|
||
string id = kv.Key.StartsWith("card_") ? kv.Key.Substring(5) : kv.Key;
|
||
double left = WorldSystem.RuinedSecondsLeft(id);
|
||
if (left <= 0) { RequestRebuild(); return; }
|
||
kv.Value.text = kv.Key.StartsWith("card_")
|
||
? "Разграблено. Отстроится через " + GameData.FormatTime(left)
|
||
: WorldData.Get(id).name + "\nотстроится " + GameData.FormatTime(left);
|
||
}
|
||
}
|
||
}
|