Files
MVP1/Assets/Scripts/Meta/World/WorldMapPanel.cs
T

384 lines
20 KiB
C#
Raw Normal View History

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);
}
}
}