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

384 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}