Слияние: наши панели HUD, стройка, казармы, камера + карта мира, чат и рейтинг от Саши
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
|
||||
// Tools -> Fix Camera & HUD: один раз исправить сцену и сохранить.
|
||||
// - HUD_Canvas -> Screen Space Overlay, отвязан от камеры
|
||||
// - с камеры убраны CameraZoom / CameraController / FlyCamera, добавлена CityCamera
|
||||
// - удалены компоненты с "Missing script" (скрипты, которых в проекте уже нет)
|
||||
public static class CameraHUDFixer
|
||||
{
|
||||
// [MenuItem("Tools/Fix Camera & HUD")] -- отключено, см. RestoreOldCamera
|
||||
public static void Fix()
|
||||
{
|
||||
int missing = 0;
|
||||
var hud = GameObject.Find("HUD_Canvas");
|
||||
if (hud != null) missing += GameObjectUtility.RemoveMonoBehavioursWithMissingScript(hud);
|
||||
if (Camera.main != null) missing += GameObjectUtility.RemoveMonoBehavioursWithMissingScript(Camera.main.gameObject);
|
||||
|
||||
int changes = HUDCameraSetup.Apply(true);
|
||||
|
||||
if (hud != null) EditorSceneManager.MarkSceneDirty(hud.scene);
|
||||
else if (Camera.main != null) EditorSceneManager.MarkSceneDirty(Camera.main.gameObject.scene);
|
||||
|
||||
Debug.Log("Камера и HUD исправлены: изменений " + changes + ", удалено пустых скриптов " + missing +
|
||||
". Сохрани сцену (Ctrl+S). Границы карты камеры: компонент CityCamera на Main Camera.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43380bdc85562cc4abb95b707e06cfb6
|
||||
@@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Events;
|
||||
using UnityEditor.SceneManagement;
|
||||
using TMPro;
|
||||
|
||||
// Кладётся в Assets/Editor/.
|
||||
// Tools -> Build HUD Panels:
|
||||
// 1) находит HUD_Canvas,
|
||||
// 2) создаёт для каждой кнопки свою панель (заголовок + крестик + область Content),
|
||||
// 3) вешает на панель её скрипт (QuestsPanel, ShopPanel...) -- он сам рисует содержимое в игре,
|
||||
// 4) вешает UIPanelManager и подключает onClick ВСЕХ кнопок по их именам,
|
||||
// 5) добавляет красные значки (HUDBadge) на кнопки, где бывает "что забрать".
|
||||
// Можно запускать повторно -- старые панели пересоздаются.
|
||||
public class HUDPanelsBuilder
|
||||
{
|
||||
class Entry
|
||||
{
|
||||
public string button, id, title; public Type panel; public bool badge;
|
||||
public Entry(string button, string id, string title, Type panel, bool badge)
|
||||
{ this.button = button; this.id = id; this.title = title; this.panel = panel; this.badge = badge; }
|
||||
}
|
||||
|
||||
// имя кнопки в иерархии -> id панели, заголовок, скрипт панели, нужен ли значок
|
||||
static readonly Entry[] Map =
|
||||
{
|
||||
new Entry("RightBtn_Quests", "Quests", "ЗАДАНИЯ", typeof(QuestsPanel), true),
|
||||
new Entry("RightBtn_Events", "Events", "СОБЫТИЯ", typeof(EventsPanel), true),
|
||||
new Entry("RightBtn_Rewards", "Rewards", "НАГРАДЫ", typeof(RewardsPanel), true),
|
||||
new Entry("NavBtn_Mail", "Mail", "ПОЧТА", typeof(MailPanel), true),
|
||||
new Entry("SettingsButton", "Settings", "НАСТРОЙКИ", typeof(SettingsPanel), false),
|
||||
new Entry("PlayerAvatar", "Profile", "ПРОФИЛЬ", typeof(ProfilePanel), false),
|
||||
new Entry("Nav_Hero", "Hero", "ГЕРОЙ", typeof(HeroPanel), true),
|
||||
new Entry("Nav_Battle", "Battle", "БОЙ", typeof(BattlePanel), false),
|
||||
new Entry("Nav_Arena", "Arena", "АРЕНА", typeof(ArenaPanel), true),
|
||||
new Entry("Nav_Troops", "Troops", "ВОЙСКА", typeof(TroopsPanel), false),
|
||||
new Entry("Nav_Shop", "Shop", "МАГАЗИН", typeof(ShopPanel), true),
|
||||
new Entry("Nav_World", "World", "КАРТА МИРА", typeof(WorldMapPanel), true), // кнопки нет -- билдер создаст
|
||||
new Entry("Nav_Home", "Home", null, null, false), // без панели: закрывает всё
|
||||
};
|
||||
|
||||
[MenuItem("Tools/Build HUD Panels")]
|
||||
public static void Build()
|
||||
{
|
||||
GameObject canvasGO = GameObject.Find("HUD_Canvas");
|
||||
if (canvasGO == null) { Debug.LogError("Не найден объект HUD_Canvas на сцене."); return; }
|
||||
|
||||
|
||||
// казармы: префаб + данные здания + в список стройки
|
||||
EnsureBarracks();
|
||||
|
||||
// менеджер
|
||||
UIPanelManager manager = canvasGO.GetComponent<UIPanelManager>();
|
||||
if (manager == null) manager = canvasGO.AddComponent<UIPanelManager>();
|
||||
manager.panels.Clear();
|
||||
|
||||
// корень для панелей (пересоздаём)
|
||||
Transform old = canvasGO.transform.Find("Panels");
|
||||
if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject);
|
||||
GameObject root = new GameObject("Panels", typeof(RectTransform));
|
||||
root.transform.SetParent(canvasGO.transform, false);
|
||||
Stretch(root.GetComponent<RectTransform>(), Vector4.zero);
|
||||
|
||||
// Корень сам не блокирует клики (нет Image), поэтому нижняя/верхняя панели HUD остаются живыми.
|
||||
int wired = 0;
|
||||
string missing = "";
|
||||
foreach (var m in Map)
|
||||
{
|
||||
if (m.title != null)
|
||||
{
|
||||
// карте мира нужно больше места -- её окно почти на весь экран между верхней и нижней панелями
|
||||
Vector4 offsets = m.id == "World" ? new Vector4(16, 145, 16, 195) : new Vector4(30, 250, 30, 230);
|
||||
GameObject panel = CreatePanel(root.transform, m.id, m.title, manager, offsets);
|
||||
if (m.panel != null) panel.AddComponent(m.panel);
|
||||
manager.panels.Add(new UIPanelManager.PanelEntry { id = m.id, panel = panel });
|
||||
}
|
||||
|
||||
Button btn = FindButton(canvasGO.transform, m.button);
|
||||
if (btn == null && m.id == "World") btn = CreateWorldButton(canvasGO.transform, root.transform);
|
||||
if (btn == null) { Debug.LogWarning("Кнопка не найдена: " + m.button); missing += " " + m.button; continue; }
|
||||
|
||||
// убираем старые подписки из Inspector и вешаем свою
|
||||
for (int i = btn.onClick.GetPersistentEventCount() - 1; i >= 0; i--)
|
||||
UnityEventTools.RemovePersistentListener(btn.onClick, i);
|
||||
UnityEventTools.AddStringPersistentListener(btn.onClick, manager.Open, m.id);
|
||||
|
||||
// красный значок "есть что забрать"
|
||||
HUDBadge badge = btn.GetComponent<HUDBadge>();
|
||||
if (m.badge)
|
||||
{
|
||||
if (badge == null) badge = btn.gameObject.AddComponent<HUDBadge>();
|
||||
badge.panelId = m.id;
|
||||
}
|
||||
else if (badge != null) UnityEngine.Object.DestroyImmediate(badge);
|
||||
|
||||
wired++;
|
||||
}
|
||||
|
||||
// Кнопка "СТРОЙКА": включает/выключает режим стройки (тап по участку -> выбор здания).
|
||||
// Старое меню стройки прячем: при запуске игры оно больше не открывается.
|
||||
GameObject buildMenu = null;
|
||||
foreach (var t in canvasGO.GetComponentsInChildren<Transform>(true))
|
||||
if (t.name == "BuildMenuPanel") { buildMenu = t.gameObject; break; }
|
||||
if (buildMenu != null) buildMenu.SetActive(false);
|
||||
|
||||
BuildModeController buildMode = canvasGO.GetComponent<BuildModeController>();
|
||||
if (buildMode == null) buildMode = canvasGO.AddComponent<BuildModeController>();
|
||||
Button buildBtn = FindButton(canvasGO.transform, "Nav_Build");
|
||||
if (buildBtn == null)
|
||||
buildBtn = CreateSideButton(canvasGO.transform, root.transform, "Nav_Build", "СТРОЙКА",
|
||||
"Assets/UI/HUD v2/Рюкзак/v1.png", new Vector2(30, 400), new Color(0.55f, 0.4f, 0.2f));
|
||||
for (int i = buildBtn.onClick.GetPersistentEventCount() - 1; i >= 0; i--)
|
||||
UnityEventTools.RemovePersistentListener(buildBtn.onClick, i);
|
||||
UnityEventTools.AddVoidPersistentListener(buildBtn.onClick, buildMode.ToggleMode);
|
||||
buildMode.modeButton = buildBtn;
|
||||
buildMode.legacyBuildMenu = buildMenu;
|
||||
EditorUtility.SetDirty(buildMode);
|
||||
|
||||
// Меню стройки растянуто на весь экран и закрывало все кнопки HUD (ловило клики и белило экран).
|
||||
// Делаем его фон прозрачным и "сквозным" -- кнопки зданий внутри продолжают работать.
|
||||
foreach (string see in new[] { "BuildMenuPanel", "ButtonContainer" })
|
||||
{
|
||||
foreach (var t in canvasGO.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
if (t.name != see) continue;
|
||||
var img = t.GetComponent<Image>();
|
||||
if (img == null) continue;
|
||||
var c = img.color; c.a = 0f; img.color = c;
|
||||
img.raycastTarget = false;
|
||||
EditorUtility.SetDirty(img);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(manager);
|
||||
EditorSceneManager.MarkSceneDirty(canvasGO.scene);
|
||||
Debug.Log("Готово: панелей " + manager.panels.Count + ", подключено кнопок " + wired + " из " + Map.Length +
|
||||
(missing.Length > 0 ? ". Не найдены:" + missing : "") + ". Не забудь сохранить сцену (Ctrl+S).");
|
||||
}
|
||||
|
||||
// ===================== КАЗАРМЫ =====================
|
||||
|
||||
const string BarracksPrefabPath = "Assets/Prefab/Buildings/Barracks.prefab";
|
||||
const string BarracksDataPath = "Assets/Prefab/Buildings/Data_Barracks.asset";
|
||||
|
||||
[MenuItem("Tools/Create Barracks")]
|
||||
public static void EnsureBarracks()
|
||||
{
|
||||
Sprite icon = LoadSprite("Assets/UI/HUD v2/Войска/v1.png");
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(BarracksPrefabPath);
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
// берём готовое здание как шаблон: тот же масштаб, слой, материал, коллайдер
|
||||
string template = "Assets/Prefab/Buildings/Food.prefab";
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(template) == null)
|
||||
{
|
||||
var bmT = UnityEngine.Object.FindAnyObjectByType<BuildManager>();
|
||||
if (bmT != null)
|
||||
foreach (var b in bmT.allBuildings)
|
||||
if (b != null && b.prefab != null) { template = AssetDatabase.GetAssetPath(b.prefab); break; }
|
||||
}
|
||||
if (!AssetDatabase.CopyAsset(template, BarracksPrefabPath))
|
||||
{
|
||||
Debug.LogError("Не удалось создать префаб казарм из " + template);
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject root = PrefabUtility.LoadPrefabContents(BarracksPrefabPath);
|
||||
root.name = "Barracks";
|
||||
foreach (var c in root.GetComponents<ResourceCollector>()) UnityEngine.Object.DestroyImmediate(c);
|
||||
foreach (var c in root.GetComponents<ResourceProducer>()) UnityEngine.Object.DestroyImmediate(c);
|
||||
foreach (var c in root.GetComponents<BuildingUI>()) UnityEngine.Object.DestroyImmediate(c);
|
||||
var kids = new System.Collections.Generic.List<GameObject>();
|
||||
foreach (Transform ch in root.transform) kids.Add(ch.gameObject); // полоска запаса ресурса -- не нужна
|
||||
foreach (var k in kids) UnityEngine.Object.DestroyImmediate(k);
|
||||
|
||||
var sr = root.GetComponent<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
{
|
||||
if (icon != null)
|
||||
{
|
||||
Vector2 oldSize = sr.sprite != null ? (Vector2)sr.sprite.bounds.size : Vector2.one;
|
||||
sr.sprite = icon;
|
||||
sr.color = Color.white;
|
||||
float k = Mathf.Min(oldSize.x / icon.bounds.size.x, oldSize.y / icon.bounds.size.y);
|
||||
root.transform.localScale = Vector3.Scale(root.transform.localScale, new Vector3(k, k, 1));
|
||||
}
|
||||
else sr.color = new Color(1f, 0.6f, 0.6f); // нет иконки -- хотя бы отличаем цветом
|
||||
|
||||
var box = root.GetComponent<BoxCollider2D>();
|
||||
if (box != null && sr.sprite != null) { box.size = sr.sprite.bounds.size; box.offset = sr.sprite.bounds.center; }
|
||||
}
|
||||
if (root.GetComponent<Barracks>() == null) root.AddComponent<Barracks>();
|
||||
PrefabUtility.SaveAsPrefabAsset(root, BarracksPrefabPath);
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(BarracksPrefabPath);
|
||||
Debug.Log("Создан префаб казарм: " + BarracksPrefabPath);
|
||||
}
|
||||
|
||||
BuildingData data = AssetDatabase.LoadAssetAtPath<BuildingData>(BarracksDataPath);
|
||||
if (data == null)
|
||||
{
|
||||
data = ScriptableObject.CreateInstance<BuildingData>();
|
||||
data.buildingName = "Казармы";
|
||||
data.icon = icon;
|
||||
data.prefab = prefab;
|
||||
data.requiredTownHallLevel = 1;
|
||||
data.cost = new[]
|
||||
{
|
||||
new ResourceCost { type = ResourceType.Wood, amount = 300 },
|
||||
new ResourceCost { type = ResourceType.Stone, amount = 200 },
|
||||
};
|
||||
AssetDatabase.CreateAsset(data, BarracksDataPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("Созданы данные здания: " + BarracksDataPath);
|
||||
}
|
||||
|
||||
var bm = UnityEngine.Object.FindAnyObjectByType<BuildManager>();
|
||||
if (bm != null && !bm.allBuildings.Contains(data))
|
||||
{
|
||||
bm.allBuildings.Add(data);
|
||||
EditorUtility.SetDirty(bm);
|
||||
EditorSceneManager.MarkSceneDirty(bm.gameObject.scene);
|
||||
Debug.Log("Казармы добавлены в список стройки (BuildManager)");
|
||||
}
|
||||
}
|
||||
|
||||
static Sprite LoadSprite(string path)
|
||||
{
|
||||
foreach (var a in AssetDatabase.LoadAllAssetsAtPath(path))
|
||||
if (a is Sprite) return (Sprite)a;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ищем объект по имени среди всех детей (включая выключенные); Button может быть на нём или внутри
|
||||
static Button FindButton(Transform root, string name)
|
||||
{
|
||||
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
if (t.name != name) continue;
|
||||
Button b = t.GetComponent<Button>();
|
||||
if (b == null) b = t.GetComponentInChildren<Button>(true);
|
||||
if (b == null) b = t.gameObject.AddComponent<Button>(); // на случай если там просто Image
|
||||
return b;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Кнопка "МИР" слева над нижней панелью (если дизайнер ещё не сделал свою с именем Nav_World)
|
||||
static Button CreateWorldButton(Transform canvas, Transform panelsRoot)
|
||||
{
|
||||
return CreateSideButton(canvas, panelsRoot, "Nav_World", "МИР", "Assets/UI/HUD v2/Мир/v1.png",
|
||||
new Vector2(30, 210), new Color(0.2f, 0.45f, 0.3f));
|
||||
}
|
||||
|
||||
static Button CreateSideButton(Transform canvas, Transform panelsRoot, string objName, string label, string spritePath, Vector2 pos, Color fallback)
|
||||
{
|
||||
GameObject go = new GameObject(objName, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
go.transform.SetParent(canvas, false);
|
||||
go.transform.SetSiblingIndex(panelsRoot.GetSiblingIndex()); // под панелями, чтобы открытая панель её закрывала
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
rt.anchorMin = rt.anchorMax = rt.pivot = new Vector2(0, 0);
|
||||
rt.sizeDelta = new Vector2(150, 150);
|
||||
rt.anchoredPosition = pos;
|
||||
|
||||
var img = go.GetComponent<Image>();
|
||||
Sprite sprite = null;
|
||||
foreach (var a in AssetDatabase.LoadAllAssetsAtPath(spritePath))
|
||||
if (a is Sprite) { sprite = (Sprite)a; break; }
|
||||
if (sprite != null) { img.sprite = sprite; img.preserveAspect = true; }
|
||||
else img.color = fallback;
|
||||
|
||||
GameObject t = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
t.transform.SetParent(go.transform, false);
|
||||
var tmp = t.GetComponent<TextMeshProUGUI>();
|
||||
tmp.text = label; tmp.fontSize = 30; tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.alignment = TextAlignmentOptions.Center; tmp.color = Color.white; tmp.raycastTarget = false;
|
||||
tmp.outlineWidth = 0.25f; tmp.outlineColor = Color.black;
|
||||
RectTransform tr = t.GetComponent<RectTransform>();
|
||||
tr.anchorMin = new Vector2(0, 0); tr.anchorMax = new Vector2(1, 0); tr.pivot = new Vector2(0.5f, 1);
|
||||
tr.sizeDelta = new Vector2(0, 40); tr.anchoredPosition = new Vector2(0, 8);
|
||||
return go.GetComponent<Button>();
|
||||
}
|
||||
|
||||
static GameObject CreatePanel(Transform parent, string id, string title, UIPanelManager manager, Vector4 offsets)
|
||||
{
|
||||
// окно оставляет сверху и снизу место под TopPanel и BottomNavBar, чтобы они были кликабельны
|
||||
GameObject go = new GameObject("Panel_" + id, typeof(RectTransform), typeof(Image));
|
||||
go.transform.SetParent(parent, false);
|
||||
go.GetComponent<Image>().color = new Color(0.1f, 0.12f, 0.15f, 0.97f);
|
||||
Stretch(go.GetComponent<RectTransform>(), offsets); // left, top, right, bottom
|
||||
|
||||
// заголовок
|
||||
GameObject t = new GameObject("Title", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
t.transform.SetParent(go.transform, false);
|
||||
var tmp = t.GetComponent<TextMeshProUGUI>();
|
||||
tmp.text = title; tmp.fontSize = 52; tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.alignment = TextAlignmentOptions.Center; tmp.color = new Color(0.95f, 0.75f, 0.25f);
|
||||
tmp.raycastTarget = false;
|
||||
RectTransform tr = t.GetComponent<RectTransform>();
|
||||
tr.anchorMin = new Vector2(0, 1); tr.anchorMax = new Vector2(1, 1); tr.pivot = new Vector2(0.5f, 1);
|
||||
tr.sizeDelta = new Vector2(0, 110); tr.anchoredPosition = Vector2.zero;
|
||||
|
||||
// область содержимого: сюда скрипт панели рисует списки, кнопки и т.д.
|
||||
GameObject content = new GameObject("Content", typeof(RectTransform));
|
||||
content.transform.SetParent(go.transform, false);
|
||||
Stretch(content.GetComponent<RectTransform>(), new Vector4(20, 120, 20, 20));
|
||||
|
||||
// крестик
|
||||
GameObject c = new GameObject("CloseButton", typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
c.transform.SetParent(go.transform, false);
|
||||
c.GetComponent<Image>().color = new Color(0.6f, 0.15f, 0.15f);
|
||||
RectTransform cr = c.GetComponent<RectTransform>();
|
||||
cr.anchorMin = cr.anchorMax = cr.pivot = new Vector2(1, 1);
|
||||
cr.sizeDelta = new Vector2(90, 90); cr.anchoredPosition = new Vector2(-12, -10);
|
||||
GameObject x = new GameObject("X", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
x.transform.SetParent(c.transform, false);
|
||||
var xt = x.GetComponent<TextMeshProUGUI>();
|
||||
xt.text = "X"; xt.fontSize = 44; xt.alignment = TextAlignmentOptions.Center; xt.color = Color.white;
|
||||
xt.raycastTarget = false;
|
||||
Stretch(x.GetComponent<RectTransform>(), Vector4.zero);
|
||||
UnityEventTools.AddVoidPersistentListener(c.GetComponent<Button>().onClick, manager.CloseAll);
|
||||
|
||||
go.SetActive(false);
|
||||
return go;
|
||||
}
|
||||
|
||||
// left, top, right, bottom -- отступы от краёв родителя
|
||||
static void Stretch(RectTransform rt, Vector4 ltrb)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = new Vector2(ltrb.x, ltrb.w);
|
||||
rt.offsetMax = new Vector2(-ltrb.z, -ltrb.y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26bcfec97167cdb4b9dee113e5f85abc
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
|
||||
// HUD ПРИКРЕПЛЁН К КАМЕРЕ (Canvas: Screen Space - Camera):
|
||||
// - HUD рисуется камерой и всегда занимает экран одинаково -- при зуме и перемещении не плывёт и не масштабируется
|
||||
// - карту можно двигать и зумить (CameraController), клики по кнопкам HUD работают
|
||||
// - спрайты внутри HUD (например "Рамка в середине") рисуются правильно -- в этом режиме Canvas живёт в мире перед камерой
|
||||
// - CameraZoom больше не нужен (он масштабировал HUD вручную и делал это в обратную сторону)
|
||||
//
|
||||
// Применяется сам один раз после компиляции, если сцена ещё не в этом режиме. Вручную: Tools -> HUD: Attach To Camera.
|
||||
[InitializeOnLoad]
|
||||
public static class RestoreOldCamera
|
||||
{
|
||||
static readonly Vector2 DesignSize = new Vector2(2007.7732f, 1032.786f); // размер, под который собран HUD
|
||||
|
||||
static RestoreOldCamera() { EditorApplication.delayCall += Auto; }
|
||||
|
||||
static void Auto()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode) return;
|
||||
var hud = GameObject.Find("HUD_Canvas");
|
||||
var cam = Camera.main;
|
||||
if (hud == null || cam == null) return;
|
||||
var canvas = hud.GetComponent<Canvas>();
|
||||
var sc = hud.GetComponent<CanvasScaler>();
|
||||
bool ok = canvas != null && canvas.renderMode == RenderMode.ScreenSpaceCamera && canvas.worldCamera == cam
|
||||
&& cam.GetComponent<CameraZoom>() == null && cam.GetComponent<CityCamera>() == null
|
||||
&& sc != null && sc.enabled && sc.screenMatchMode == CanvasScaler.ScreenMatchMode.Expand;
|
||||
if (!ok) Apply();
|
||||
}
|
||||
|
||||
[MenuItem("Tools/HUD: Attach To Camera")]
|
||||
public static void Apply()
|
||||
{
|
||||
var cam = Camera.main;
|
||||
var hud = GameObject.Find("HUD_Canvas");
|
||||
if (cam == null || hud == null) { Debug.LogError("Нужны Main Camera и HUD_Canvas"); return; }
|
||||
|
||||
// ---- камера: только CameraController (перемещение + зум) ----
|
||||
foreach (var c in cam.GetComponents<CityCamera>()) Object.DestroyImmediate(c);
|
||||
foreach (var c in cam.GetComponents<CameraZoom>()) Object.DestroyImmediate(c);
|
||||
var ctrl = cam.GetComponent<CameraController>();
|
||||
if (ctrl == null)
|
||||
{
|
||||
ctrl = cam.gameObject.AddComponent<CameraController>();
|
||||
ctrl.moveSpeed = 5f; ctrl.zoomSpeed = 2f; ctrl.minOrthoSize = 2f; ctrl.maxOrthoSize = 50f;
|
||||
}
|
||||
ctrl.enabled = true;
|
||||
cam.orthographic = true;
|
||||
cam.transform.localScale = Vector3.one;
|
||||
cam.transform.rotation = Quaternion.identity;
|
||||
if (cam.orthographicSize < 2f || cam.orthographicSize > 50f) cam.orthographicSize = 10f;
|
||||
|
||||
// ---- HUD: Screen Space - Camera ----
|
||||
var canvas = hud.GetComponent<Canvas>();
|
||||
var rt = (RectTransform)hud.transform;
|
||||
if (rt.parent != null) rt.SetParent(null, false);
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = cam;
|
||||
canvas.planeDistance = 5f; // между камерой (z=-10) и картой (z=0)
|
||||
canvas.sortingOrder = 100; // поверх зданий
|
||||
rt.localRotation = Quaternion.identity;
|
||||
|
||||
var scaler = hud.GetComponent<CanvasScaler>();
|
||||
if (scaler == null) scaler = hud.AddComponent<CanvasScaler>();
|
||||
scaler.enabled = true;
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = DesignSize;
|
||||
// Expand: весь HUD (2007x1032) всегда помещается на экран целиком -- и в узком окне, и на широком телефоне
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||||
|
||||
// спрайты внутри HUD -- поверх карты
|
||||
foreach (var sr in hud.GetComponentsInChildren<SpriteRenderer>(true))
|
||||
{
|
||||
if (sr.sortingOrder < 100) sr.sortingOrder = 100;
|
||||
if (!sr.gameObject.activeSelf) sr.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(hud.scene);
|
||||
Debug.Log("HUD прикреплён к камере (Screen Space - Camera), CameraZoom убран. Сохрани сцену: Ctrl+S.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4f66fb7b626a0e41af6fe3fe793e917
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
// Следит, чтобы сцена карты мира (WorldMap, от Саши) была в Build Settings --
|
||||
// иначе переход город -> карта не работает. Срабатывает сам после каждой компиляции.
|
||||
[InitializeOnLoad]
|
||||
public static class SceneListFixer
|
||||
{
|
||||
const string CityScene = "Assets/Scenes/MVP1.unity";
|
||||
const string WorldScene = "Assets/Scenes/WorldMap.unity";
|
||||
|
||||
static SceneListFixer() { EditorApplication.delayCall += Ensure; }
|
||||
|
||||
static void Ensure()
|
||||
{
|
||||
var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes);
|
||||
bool changed = false;
|
||||
foreach (var path in new[] { CityScene, WorldScene })
|
||||
{
|
||||
if (AssetDatabase.LoadAssetAtPath<SceneAsset>(path) == null) continue;
|
||||
if (scenes.Exists(s => s.path == path)) continue;
|
||||
scenes.Add(new EditorBuildSettingsScene(path, true));
|
||||
changed = true;
|
||||
}
|
||||
// город должен быть первым (с него стартует игра)
|
||||
int city = scenes.FindIndex(s => s.path == CityScene);
|
||||
if (city > 0) { var c = scenes[city]; scenes.RemoveAt(city); scenes.Insert(0, c); changed = true; }
|
||||
if (changed)
|
||||
{
|
||||
EditorBuildSettings.scenes = scenes.ToArray();
|
||||
UnityEngine.Debug.Log("[СЦЕНЫ] Build Settings: город + карта мира добавлены");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89a48f1e99f1cd4469929f3c418ce55b
|
||||
@@ -0,0 +1,521 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Tools -> Build World Map Scene
|
||||
// 1. Создаёт сцену Assets/Scenes/WorldMap.unity: камера, EventSystem, UI карты, WorldMapController.
|
||||
// 2. Ставит MVP1 и WorldMap в Build Settings (иначе SceneManager.LoadScene их не найдёт).
|
||||
// 3. Проверяет, что на кнопке Nav_Battle в городе висит OpenBattleButton.
|
||||
// Можно запускать повторно -- сцена карты пересоздаётся заново.
|
||||
public static class WorldMapSceneBuilder
|
||||
{
|
||||
const string WorldMapScenePath = "Assets/Scenes/WorldMap.unity";
|
||||
const string CityScenePath = "Assets/Scenes/MVP1.unity";
|
||||
|
||||
[MenuItem("Tools/Build World Map Scene")]
|
||||
public static void Build()
|
||||
{
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return;
|
||||
|
||||
CreateWorldMapScene();
|
||||
UpdateBuildSettings();
|
||||
HookUpCityBattleButton();
|
||||
|
||||
Debug.Log("[КАРТА] Сцена карты мира создана, кнопка БОЙ в городе подключена. Запускай MVP1 и жми БОЙ.");
|
||||
}
|
||||
|
||||
// ---------------- СЦЕНА КАРТЫ ----------------
|
||||
|
||||
static void CreateWorldMapScene()
|
||||
{
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
|
||||
var camGO = new GameObject("Main Camera", typeof(Camera), typeof(MapCameraController));
|
||||
camGO.tag = "MainCamera";
|
||||
camGO.transform.position = new Vector3(0, 0, -10);
|
||||
Camera cam = camGO.GetComponent<Camera>();
|
||||
cam.orthographic = true;
|
||||
cam.orthographicSize = 8f;
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = new Color(0.12f, 0.16f, 0.12f);
|
||||
|
||||
new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
||||
|
||||
var canvasGO = new GameObject("WorldMap_Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
canvasGO.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
CanvasScaler scaler = canvasGO.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
RectTransform canvasRect = canvasGO.GetComponent<RectTransform>();
|
||||
|
||||
var controller = new GameObject("WorldMapController").AddComponent<WorldMapController>();
|
||||
controller.mapCamera = camGO.GetComponent<MapCameraController>();
|
||||
|
||||
// ---- Верхняя панель: назад | войско | мой город ----
|
||||
RectTransform topBar = CreatePanel(canvasRect, "TopBar", new Color(0.08f, 0.08f, 0.1f, 0.85f));
|
||||
AnchorStretch(topBar, top: true, height: 110);
|
||||
|
||||
controller.soldiersText = CreateText(topBar, "SoldiersText", "Войска дома: 30", 40);
|
||||
|
||||
RectTransform back = CreateButton(topBar, "BackButton", "НАЗАД", new Color(0.3f, 0.3f, 0.33f), out _);
|
||||
AnchorSide(back, left: true, new Vector2(220, 80), 20);
|
||||
controller.backButton = back.GetComponent<Button>();
|
||||
|
||||
RectTransform myCity = CreateButton(topBar, "MyCityButton", "МОЙ ГОРОД", new Color(0.25f, 0.45f, 0.8f), out _);
|
||||
AnchorSide(myCity, left: false, new Vector2(280, 80), 20);
|
||||
controller.myCityButton = myCity.GetComponent<Button>();
|
||||
|
||||
// ---- Подсказка (над нижней панелью, правее чата) ----
|
||||
RectTransform hintBar = CreatePanel(canvasRect, "HintBar", new Color(0.08f, 0.08f, 0.1f, 0.75f));
|
||||
SetRect(hintBar, new Vector2(0, 0), new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
|
||||
hintBar.offsetMin = new Vector2(ChatWidth + 24, 172);
|
||||
hintBar.offsetMax = new Vector2(-12, 222);
|
||||
controller.hintText = CreateText(hintBar, "Hint", "", 28);
|
||||
|
||||
// ---- Чат слева внизу ----
|
||||
BuildChat(canvasRect);
|
||||
|
||||
// ---- Нижняя панель: Карта мира | События | Круг | Поселение ----
|
||||
BuildNavBar(canvasRect, controller);
|
||||
|
||||
// ---- Кнопка "Рейтинг" слева ----
|
||||
RectTransform ratingBtn = CreateButton(canvasRect, "RatingButton", "Рейтинг", new Color(0.12f, 0.2f, 0.4f, 0.95f), out _);
|
||||
SetRect(ratingBtn, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(12, -122), new Vector2(210, 110));
|
||||
controller.ratingButton = ratingBtn.GetComponent<Button>();
|
||||
|
||||
// ---- Панель рейтинга (выше всего, кроме окна атаки и уведомлений) ----
|
||||
controller.ratingPanel = BuildRatingPanel(canvasRect);
|
||||
|
||||
// ---- Окно атаки ----
|
||||
RectTransform attack = CreateCenterPanel(canvasRect, "AttackPanel", new Vector2(900, 640));
|
||||
controller.attackPanel = attack.gameObject;
|
||||
|
||||
RectTransform attackTitle = CreateTextRect(attack, "Title", "Замок", 56);
|
||||
AnchorTop(attackTitle, 110, 20);
|
||||
controller.attackTitleText = attackTitle.GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform attackInfo = CreateTextRect(attack, "Info", "", 32);
|
||||
AnchorTop(attackInfo, 170, 130);
|
||||
controller.attackInfoText = attackInfo.GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform sendCount = CreateTextRect(attack, "SoldiersToSend", "Отправить: 30 из 30", 36);
|
||||
AnchorTop(sendCount, 60, 310);
|
||||
controller.soldiersToSendText = sendCount.GetComponent<TMP_Text>();
|
||||
|
||||
GameObject sliderGO = DefaultControls.CreateSlider(new DefaultControls.Resources());
|
||||
sliderGO.name = "SoldiersSlider";
|
||||
RectTransform sliderRect = sliderGO.GetComponent<RectTransform>();
|
||||
sliderRect.SetParent(attack, false);
|
||||
AnchorTop(sliderRect, 50, 390);
|
||||
sliderRect.offsetMin = new Vector2(80, sliderRect.offsetMin.y);
|
||||
sliderRect.offsetMax = new Vector2(-80, sliderRect.offsetMax.y);
|
||||
StyleSlider(sliderGO);
|
||||
controller.soldiersSlider = sliderGO.GetComponent<Slider>();
|
||||
|
||||
RectTransform send = CreateButton(attack, "SendButton", "В ПОХОД", new Color(0.75f, 0.25f, 0.15f), out _);
|
||||
AnchorBottom(send, new Vector2(320, 100), new Vector2(170, 30));
|
||||
controller.sendButton = send.GetComponent<Button>();
|
||||
|
||||
RectTransform cancel = CreateButton(attack, "CancelButton", "ОТМЕНА", new Color(0.3f, 0.3f, 0.33f), out _);
|
||||
AnchorBottom(cancel, new Vector2(320, 100), new Vector2(-170, 30));
|
||||
controller.cancelButton = cancel.GetComponent<Button>();
|
||||
|
||||
attack.gameObject.SetActive(false);
|
||||
|
||||
// ---- Уведомление ----
|
||||
RectTransform popup = CreateCenterPanel(canvasRect, "PopupPanel", new Vector2(860, 520));
|
||||
controller.popupPanel = popup.gameObject;
|
||||
|
||||
RectTransform popupTitle = CreateTextRect(popup, "Title", "ПОБЕДА", 72);
|
||||
AnchorTop(popupTitle, 120, 20);
|
||||
controller.popupTitleText = popupTitle.GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform popupBody = CreateTextRect(popup, "Body", "", 32);
|
||||
popupBody.offsetMin = new Vector2(40, 140);
|
||||
popupBody.offsetMax = new Vector2(-40, -140);
|
||||
controller.popupBodyText = popupBody.GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform ok = CreateButton(popup, "OkButton", "OK", new Color(0.2f, 0.5f, 0.25f), out _);
|
||||
AnchorBottom(ok, new Vector2(300, 100), new Vector2(0, 30));
|
||||
controller.popupOkButton = ok.GetComponent<Button>();
|
||||
|
||||
popup.gameObject.SetActive(false);
|
||||
|
||||
EditorSceneManager.SaveScene(scene, WorldMapScenePath);
|
||||
}
|
||||
|
||||
// ---------------- ЧАТ ----------------
|
||||
|
||||
const float ChatWidth = 620f;
|
||||
|
||||
static void BuildChat(RectTransform canvasRect)
|
||||
{
|
||||
RectTransform panel = CreatePanel(canvasRect, "ChatPanel", new Color(0.05f, 0.06f, 0.09f, 0.9f));
|
||||
SetRect(panel, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(12, 12), new Vector2(ChatWidth, 350));
|
||||
ChatPanel chat = panel.gameObject.AddComponent<ChatPanel>();
|
||||
|
||||
RectTransform worldTab = CreateButton(panel, "WorldTab", "Общий мир", new Color(0.16f, 0.35f, 0.7f), out _);
|
||||
SetRect(worldTab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(10, -10), new Vector2(220, 54));
|
||||
chat.worldTabButton = worldTab.GetComponent<Button>();
|
||||
|
||||
RectTransform circleTab = CreateButton(panel, "CircleTab", "Круг", new Color(0.12f, 0.13f, 0.17f), out _);
|
||||
SetRect(circleTab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(240, -10), new Vector2(220, 54));
|
||||
chat.circleTabButton = circleTab.GetComponent<Button>();
|
||||
|
||||
// Лента: текст прижат к низу, старые строки уходят вверх и обрезаются маской
|
||||
var messagesGO = new GameObject("Messages", typeof(RectTransform), typeof(RectMask2D));
|
||||
RectTransform messages = messagesGO.GetComponent<RectTransform>();
|
||||
messages.SetParent(panel, false);
|
||||
messages.anchorMin = Vector2.zero;
|
||||
messages.anchorMax = Vector2.one;
|
||||
messages.offsetMin = new Vector2(14, 86);
|
||||
messages.offsetMax = new Vector2(-14, -74);
|
||||
|
||||
RectTransform messagesText = CreateTextRect(messages, "MessagesText", "", 24, TextAlignmentOptions.BottomLeft);
|
||||
TMP_Text tmp = messagesText.GetComponent<TMP_Text>();
|
||||
tmp.overflowMode = TextOverflowModes.Overflow;
|
||||
chat.messagesText = tmp;
|
||||
|
||||
// Поле ввода
|
||||
GameObject inputGO = TMP_DefaultControls.CreateInputField(new TMP_DefaultControls.Resources());
|
||||
inputGO.name = "InputField";
|
||||
RectTransform inputRect = inputGO.GetComponent<RectTransform>();
|
||||
inputRect.SetParent(panel, false);
|
||||
SetRect(inputRect, Vector2.zero, new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
|
||||
inputRect.offsetMin = new Vector2(12, 12);
|
||||
inputRect.offsetMax = new Vector2(-124, 74);
|
||||
inputGO.GetComponent<Image>().color = new Color(0.12f, 0.13f, 0.17f);
|
||||
|
||||
TMP_InputField input = inputGO.GetComponent<TMP_InputField>();
|
||||
input.pointSize = 26;
|
||||
input.textComponent.color = Color.white;
|
||||
((TMP_Text)input.placeholder).text = "Введите сообщение...";
|
||||
((TMP_Text)input.placeholder).color = new Color(1f, 1f, 1f, 0.35f);
|
||||
chat.inputField = input;
|
||||
|
||||
RectTransform send = CreateButton(panel, "SendButton", ">", new Color(0.2f, 0.45f, 0.85f), out _);
|
||||
SetRect(send, new Vector2(1, 0), new Vector2(1, 0), new Vector2(1, 0), new Vector2(-12, 12), new Vector2(100, 62));
|
||||
chat.sendButton = send.GetComponent<Button>();
|
||||
}
|
||||
|
||||
// ---------------- НИЖНЯЯ ПАНЕЛЬ ----------------
|
||||
|
||||
static void BuildNavBar(RectTransform canvasRect, WorldMapController controller)
|
||||
{
|
||||
RectTransform bar = CreatePanel(canvasRect, "BottomNavBar", new Color(0.06f, 0.07f, 0.1f, 0.92f));
|
||||
SetRect(bar, Vector2.zero, new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
|
||||
bar.offsetMin = new Vector2(ChatWidth + 24, 12);
|
||||
bar.offsetMax = new Vector2(-12, 162);
|
||||
|
||||
var layout = bar.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||||
layout.padding = new RectOffset(10, 10, 10, 10);
|
||||
layout.spacing = 10;
|
||||
layout.childForceExpandWidth = true;
|
||||
layout.childForceExpandHeight = true;
|
||||
layout.childControlWidth = true;
|
||||
layout.childControlHeight = true;
|
||||
|
||||
Color navColor = new Color(0.14f, 0.16f, 0.22f);
|
||||
controller.worldMapNavButton = CreateButton(bar, "Nav_WorldMap", "Карта мира", navColor, out _).GetComponent<Button>();
|
||||
controller.eventsNavButton = CreateButton(bar, "Nav_Events", "События", navColor, out _).GetComponent<Button>();
|
||||
controller.circleNavButton = CreateButton(bar, "Nav_Circle", "Круг", navColor, out _).GetComponent<Button>();
|
||||
controller.settlementNavButton = CreateButton(bar, "Nav_Settlement", "Поселение", new Color(0.18f, 0.25f, 0.45f), out _).GetComponent<Button>();
|
||||
}
|
||||
|
||||
// ---------------- РЕЙТИНГ ----------------
|
||||
|
||||
static readonly Color RatingBg = new Color(0.04f, 0.06f, 0.1f, 0.97f);
|
||||
static readonly Color RatingGold = new Color(0.95f, 0.8f, 0.5f);
|
||||
|
||||
const float RatingTableLeft = 340f;
|
||||
const float RatingTableRight = 25f;
|
||||
|
||||
static RatingPanel BuildRatingPanel(RectTransform canvasRect)
|
||||
{
|
||||
RectTransform panel = CreatePanel(canvasRect, "RatingPanel", RatingBg);
|
||||
SetRect(panel, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(1640, 920));
|
||||
RatingPanel rating = panel.gameObject.AddComponent<RatingPanel>();
|
||||
|
||||
// Заголовок-баннер
|
||||
RectTransform banner = CreatePanel(panel, "TitleBanner", new Color(0.12f, 0.25f, 0.55f));
|
||||
SetRect(banner, new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -15), new Vector2(640, 90));
|
||||
CreateText(banner, "Title", "Рейтинг", 60).color = RatingGold;
|
||||
|
||||
RectTransform close = CreateButton(panel, "CloseButton", "X", new Color(0.35f, 0.15f, 0.15f), out _);
|
||||
SetRect(close, new Vector2(1, 1), new Vector2(1, 1), new Vector2(1, 1), new Vector2(-15, -15), new Vector2(80, 80));
|
||||
rating.closeButton = close.GetComponent<Button>();
|
||||
|
||||
// Разделы слева -- порядок совпадает с RatingCategory
|
||||
string[] categories = { "Игроки", "Круги", "Мощь", "PvP", "Развитие", "Сезон" };
|
||||
rating.categoryButtons = new Button[categories.Length];
|
||||
for (int i = 0; i < categories.Length; i++)
|
||||
{
|
||||
RectTransform tab = CreateButton(panel, "Category_" + i, categories[i], new Color(0.1f, 0.12f, 0.17f), out _);
|
||||
SetRect(tab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(25, -130 - i * 112), new Vector2(290, 100));
|
||||
rating.categoryButtons[i] = tab.GetComponent<Button>();
|
||||
}
|
||||
|
||||
// Таблица
|
||||
var tableGO = new GameObject("Table", typeof(RectTransform));
|
||||
RectTransform table = tableGO.GetComponent<RectTransform>();
|
||||
table.SetParent(panel, false);
|
||||
table.anchorMin = Vector2.zero;
|
||||
table.anchorMax = Vector2.one;
|
||||
table.offsetMin = new Vector2(RatingTableLeft, 110);
|
||||
table.offsetMax = new Vector2(-RatingTableRight, -130);
|
||||
|
||||
RectTransform header = CreatePanel(table, "Header", new Color(0.08f, 0.1f, 0.15f));
|
||||
SetRect(header, new Vector2(0, 1), new Vector2(1, 1), new Vector2(0.5f, 1), Vector2.zero, new Vector2(0, 50));
|
||||
Color headerColor = new Color(0.8f, 0.82f, 0.88f);
|
||||
PlaceInRow(CreateTextRect(header, "Place", "Место", 26), 0, 140, 50).GetComponent<TMP_Text>().color = headerColor;
|
||||
TMP_Text nameHeader = PlaceInRow(CreateTextRect(header, "Name", "Игрок", 26, TextAlignmentOptions.Left), 225, 330, 50).GetComponent<TMP_Text>();
|
||||
nameHeader.color = headerColor;
|
||||
rating.nameColumnHeader = nameHeader;
|
||||
TMP_Text circleHeader = PlaceInRow(CreateTextRect(header, "Circle", "Круг", 26, TextAlignmentOptions.Left), 625, 320, 50).GetComponent<TMP_Text>();
|
||||
circleHeader.color = headerColor;
|
||||
rating.circleColumnHeader = circleHeader;
|
||||
PlaceInRow(CreateTextRect(header, "Power", "Мощь", 26), 950, 220, 50).GetComponent<TMP_Text>().color = headerColor;
|
||||
|
||||
rating.rows = new RatingRowView[RatingPanel.PageSize];
|
||||
for (int i = 0; i < rating.rows.Length; i++)
|
||||
{
|
||||
rating.rows[i] = BuildRatingRow(table, i);
|
||||
}
|
||||
|
||||
rating.emptyText = CreateText(table, "EmptyText", "Раздел в разработке", 40);
|
||||
rating.emptyText.gameObject.SetActive(false);
|
||||
|
||||
RectTransform status = CreateTextRect(panel, "StatusText", "", 26, TextAlignmentOptions.Left);
|
||||
SetRect(status, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(RatingTableLeft, 30), new Vector2(520, 60));
|
||||
rating.statusText = status.GetComponent<TMP_Text>();
|
||||
|
||||
// Страницы: < 1 2 3 4 5 > -- по центру под таблицей (а не под всей панелью)
|
||||
float tableCenterX = (RatingTableLeft - RatingTableRight) / 2f;
|
||||
Color pageColor = new Color(0.1f, 0.12f, 0.17f);
|
||||
|
||||
RectTransform prev = CreateButton(panel, "PrevPage", "<", pageColor, out _);
|
||||
SetRect(prev, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX - 240, 25), new Vector2(64, 64));
|
||||
rating.prevPageButton = prev.GetComponent<Button>();
|
||||
|
||||
rating.pageButtons = new Button[5];
|
||||
for (int i = 0; i < rating.pageButtons.Length; i++)
|
||||
{
|
||||
RectTransform page = CreateButton(panel, "Page_" + i, (i + 1).ToString(), pageColor, out _);
|
||||
SetRect(page, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX - 150 + i * 75, 25), new Vector2(64, 64));
|
||||
rating.pageButtons[i] = page.GetComponent<Button>();
|
||||
}
|
||||
|
||||
RectTransform next = CreateButton(panel, "NextPage", ">", pageColor, out _);
|
||||
SetRect(next, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX + 240, 25), new Vector2(64, 64));
|
||||
rating.nextPageButton = next.GetComponent<Button>();
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
return rating;
|
||||
}
|
||||
|
||||
static RatingRowView BuildRatingRow(RectTransform table, int index)
|
||||
{
|
||||
const float rowHeight = 54f;
|
||||
var view = new RatingRowView();
|
||||
|
||||
RectTransform row = CreatePanel(table, "Row_" + (index + 1), new Color(0.07f, 0.09f, 0.13f));
|
||||
SetRect(row, new Vector2(0, 1), new Vector2(1, 1), new Vector2(0.5f, 1), new Vector2(0, -56 - index * 58), new Vector2(0, rowHeight));
|
||||
view.root = row.gameObject;
|
||||
view.background = row.GetComponent<Image>();
|
||||
|
||||
RectTransform medal = CreatePanel(row, "Medal", Color.white);
|
||||
PlaceInRow(medal, 45, 50, 50);
|
||||
view.medal = medal.GetComponent<Image>();
|
||||
view.placeText = PlaceInRow(CreateTextRect(row, "Place", "1", 30), 0, 140, rowHeight).GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform avatar = CreatePanel(row, "Avatar", Color.gray);
|
||||
PlaceInRow(avatar, 150, 48, 48);
|
||||
view.avatar = avatar.GetComponent<Image>();
|
||||
view.avatarLetter = CreateText(avatar, "Letter", "В", 28);
|
||||
|
||||
view.nameText = PlaceInRow(CreateTextRect(row, "Name", "Игрок", 30, TextAlignmentOptions.Left), 225, 330, rowHeight).GetComponent<TMP_Text>();
|
||||
|
||||
RectTransform circleBanner = CreatePanel(row, "CircleBanner", Color.blue);
|
||||
PlaceInRow(circleBanner, 570, 36, 46);
|
||||
view.circleBanner = circleBanner.GetComponent<Image>();
|
||||
view.circleText = PlaceInRow(CreateTextRect(row, "Circle", "Круг", 28, TextAlignmentOptions.Left), 625, 320, rowHeight).GetComponent<TMP_Text>();
|
||||
|
||||
TMP_Text power = PlaceInRow(CreateTextRect(row, "Power", "0", 28), 950, 220, rowHeight).GetComponent<TMP_Text>();
|
||||
power.color = RatingGold;
|
||||
view.powerText = power;
|
||||
|
||||
RectTransform inspect = CreateButton(row, "InspectButton", "?", new Color(0.18f, 0.2f, 0.27f), out _);
|
||||
PlaceInRow(inspect, 1190, 56, 44);
|
||||
view.inspectButton = inspect.GetComponent<Button>();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
// Элемент строки: прижат к левому краю строки с отступом x, по вертикали по центру
|
||||
static RectTransform PlaceInRow(RectTransform rt, float x, float width, float height)
|
||||
{
|
||||
SetRect(rt, new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(x, 0), new Vector2(width, height));
|
||||
return rt;
|
||||
}
|
||||
|
||||
static void SetRect(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 position, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = anchorMin;
|
||||
rt.anchorMax = anchorMax;
|
||||
rt.pivot = pivot;
|
||||
rt.sizeDelta = size;
|
||||
rt.anchoredPosition = position;
|
||||
}
|
||||
|
||||
// ---------------- BUILD SETTINGS ----------------
|
||||
|
||||
static void UpdateBuildSettings()
|
||||
{
|
||||
var scenes = new List<EditorBuildSettingsScene>
|
||||
{
|
||||
new EditorBuildSettingsScene(CityScenePath, true),
|
||||
new EditorBuildSettingsScene(WorldMapScenePath, true)
|
||||
};
|
||||
|
||||
// Остальные уже добавленные сцены сохраняем (кроме удалённых файлов, например старой Battle)
|
||||
foreach (var existing in EditorBuildSettings.scenes)
|
||||
{
|
||||
if (existing.path == CityScenePath || existing.path == WorldMapScenePath) continue;
|
||||
if (!File.Exists(existing.path)) continue;
|
||||
scenes.Add(existing);
|
||||
}
|
||||
|
||||
EditorBuildSettings.scenes = scenes.ToArray();
|
||||
}
|
||||
|
||||
// ---------------- КНОПКА В ГОРОДЕ ----------------
|
||||
|
||||
static void HookUpCityBattleButton()
|
||||
{
|
||||
var city = EditorSceneManager.OpenScene(CityScenePath, OpenSceneMode.Single);
|
||||
|
||||
GameObject navBattle = GameObject.Find("Nav_Battle");
|
||||
if (navBattle == null)
|
||||
{
|
||||
Debug.LogWarning("[КАРТА] В сцене MVP1 не найден объект Nav_Battle -- повесь OpenBattleButton на кнопку БОЙ вручную.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (navBattle.GetComponent<OpenBattleButton>() == null)
|
||||
{
|
||||
Undo.AddComponent<OpenBattleButton>(navBattle); // RequireComponent сам добавит Button, если его нет
|
||||
EditorSceneManager.MarkSceneDirty(city);
|
||||
EditorSceneManager.SaveScene(city);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- HELPERS ----------------
|
||||
|
||||
static RectTransform CreatePanel(RectTransform parent, string name, Color color)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
|
||||
go.transform.SetParent(parent, false);
|
||||
go.GetComponent<Image>().color = color;
|
||||
return go.GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
static RectTransform CreateCenterPanel(RectTransform parent, string name, Vector2 size)
|
||||
{
|
||||
RectTransform rt = CreatePanel(parent, name, new Color(0.05f, 0.05f, 0.07f, 0.94f));
|
||||
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rt.sizeDelta = size;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static RectTransform CreateTextRect(RectTransform parent, string name, string content, int fontSize,
|
||||
TextAlignmentOptions alignment = TextAlignmentOptions.Center)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
go.transform.SetParent(parent, false);
|
||||
var tmp = go.GetComponent<TextMeshProUGUI>();
|
||||
tmp.text = content;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = alignment;
|
||||
tmp.raycastTarget = false;
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static TMP_Text CreateText(RectTransform parent, string name, string content, int fontSize)
|
||||
{
|
||||
return CreateTextRect(parent, name, content, fontSize).GetComponent<TMP_Text>();
|
||||
}
|
||||
|
||||
static RectTransform CreateButton(RectTransform parent, string name, string label, Color color, out TMP_Text labelText)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
go.transform.SetParent(parent, false);
|
||||
go.GetComponent<Image>().color = color;
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
labelText = CreateText(rt, "Label", label, 36);
|
||||
return rt;
|
||||
}
|
||||
|
||||
static void StyleSlider(GameObject sliderGO)
|
||||
{
|
||||
Image[] images = sliderGO.GetComponentsInChildren<Image>();
|
||||
foreach (var img in images)
|
||||
{
|
||||
if (img.name == "Background") img.color = new Color(0.25f, 0.25f, 0.28f);
|
||||
else if (img.name == "Fill") img.color = new Color(0.3f, 0.55f, 1f);
|
||||
else if (img.name == "Handle") img.color = Color.white;
|
||||
}
|
||||
// Ручка крупнее -- удобнее тянуть пальцем
|
||||
Transform handle = sliderGO.transform.Find("Handle Slide Area/Handle");
|
||||
if (handle != null) ((RectTransform)handle).sizeDelta = new Vector2(50, 0);
|
||||
}
|
||||
|
||||
static void AnchorStretch(RectTransform rt, bool top, float height)
|
||||
{
|
||||
float y = top ? 1 : 0;
|
||||
rt.anchorMin = new Vector2(0, y);
|
||||
rt.anchorMax = new Vector2(1, y);
|
||||
rt.pivot = new Vector2(0.5f, y);
|
||||
rt.sizeDelta = new Vector2(0, height);
|
||||
rt.anchoredPosition = Vector2.zero;
|
||||
}
|
||||
|
||||
static void AnchorSide(RectTransform rt, bool left, Vector2 size, float offset)
|
||||
{
|
||||
float x = left ? 0 : 1;
|
||||
rt.anchorMin = new Vector2(x, 0.5f);
|
||||
rt.anchorMax = new Vector2(x, 0.5f);
|
||||
rt.pivot = new Vector2(x, 0.5f);
|
||||
rt.sizeDelta = size;
|
||||
rt.anchoredPosition = new Vector2(left ? offset : -offset, 0);
|
||||
}
|
||||
|
||||
// Растянуть по ширине родителя, прижать к верху с отступом
|
||||
static void AnchorTop(RectTransform rt, float height, float topOffset)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0, 1);
|
||||
rt.anchorMax = new Vector2(1, 1);
|
||||
rt.pivot = new Vector2(0.5f, 1);
|
||||
rt.sizeDelta = new Vector2(0, height);
|
||||
rt.anchoredPosition = new Vector2(0, -topOffset);
|
||||
}
|
||||
|
||||
static void AnchorBottom(RectTransform rt, Vector2 size, Vector2 offset)
|
||||
{
|
||||
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0);
|
||||
rt.pivot = new Vector2(0.5f, 0);
|
||||
rt.sizeDelta = size;
|
||||
rt.anchoredPosition = offset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fea028791c631d4c88cccc887d23df9
|
||||
@@ -0,0 +1,154 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &8587519695404222676
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5056236775830869539}
|
||||
- component: {fileID: 7776370013680216338}
|
||||
- component: {fileID: 1708035113988781431}
|
||||
- component: {fileID: 2152423632773446457}
|
||||
m_Layer: 3
|
||||
m_Name: Barracks
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &5056236775830869539
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8587519695404222676}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -3.48, y: -1.22, z: 0}
|
||||
m_LocalScale: {x: 0.16310804, y: 0.15743585, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!212 &7776370013680216338
|
||||
SpriteRenderer:
|
||||
serializedVersion: 3
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8587519695404222676}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 0
|
||||
m_ReceiveShadows: 0
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 0
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 0
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 0
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2801366364622282508, guid: b35488e828328a54f88edf516b780ea5, type: 3}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
m_DrawMode: 0
|
||||
m_Size: {x: 1, y: 1}
|
||||
m_AdaptiveModeThreshold: 0.5
|
||||
m_SpriteTileMode: 0
|
||||
m_WasSpriteAssigned: 1
|
||||
m_SpriteSortPoint: 0
|
||||
m_BlendShapeWeights: []
|
||||
--- !u!61 &1708035113988781431
|
||||
BoxCollider2D:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8587519695404222676}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 3
|
||||
m_Density: 1
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_ForceSendLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ForceReceiveLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_ContactCaptureLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_CallbackLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_IsTrigger: 0
|
||||
m_UsedByEffector: 0
|
||||
m_CompositeOperation: 0
|
||||
m_CompositeOrder: 0
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
oldSize: {x: 12.54, y: 12.54}
|
||||
newSize: {x: 1, y: 1}
|
||||
adaptiveTilingThreshold: 0.5
|
||||
drawMode: 0
|
||||
adaptiveTiling: 0
|
||||
m_AutoTiling: 0
|
||||
m_Size: {x: 12.54, y: 12.54}
|
||||
m_EdgeRadius: 0
|
||||
--- !u!114 &2152423632773446457
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8587519695404222676}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 96f3807928801df4a840181e38da1317, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Barracks
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd259e52fed42bc48a1352e78ce97819
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6ce7334b9f0b4914ba6750d2a2d35155, type: 3}
|
||||
m_Name: Data_Barracks
|
||||
m_EditorClassIdentifier: Assembly-CSharp::BuildingData
|
||||
buildingName: "\u041A\u0430\u0437\u0430\u0440\u043C\u044B"
|
||||
icon: {fileID: 2801366364622282508, guid: b35488e828328a54f88edf516b780ea5, type: 3}
|
||||
prefab: {fileID: 8587519695404222676, guid: bd259e52fed42bc48a1352e78ce97819, type: 3}
|
||||
requiredTownHallLevel: 1
|
||||
cost:
|
||||
- type: 2
|
||||
amount: 300
|
||||
- type: 3
|
||||
amount: 200
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16e55e952b334ec488bbee6b42448828
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -237,7 +237,7 @@ MonoBehaviour:
|
||||
m_FallbackScreenDPI: 96
|
||||
m_DefaultSpriteDPI: 96
|
||||
m_DynamicPixelsPerUnit: 100
|
||||
m_PresetInfoIsWorld: 0
|
||||
m_PresetInfoIsWorld: 1
|
||||
--- !u!114 &3086347143011804087
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -720,9 +720,9 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: -6104705389903059128, guid: 2e8301040a0e9964c82256869c0a9498, type: 3}
|
||||
m_Sprite: {fileID: -767853006991775525, guid: 0f4911df226c33e40975fb78c06568f5, type: 3}
|
||||
m_Color: {r: 0.8627452, g: 0.8627452, b: 0.8627452, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
@@ -806,7 +806,7 @@ BoxCollider2D:
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
oldSize: {x: 1.7914286, y: 1.7914286}
|
||||
oldSize: {x: 3.382857, y: 3.0857143}
|
||||
newSize: {x: 1, y: 1}
|
||||
adaptiveTilingThreshold: 0.5
|
||||
drawMode: 0
|
||||
|
||||
@@ -83,10 +83,10 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2801366364622282508, guid: 7ba171d8f7d5a8c498a0f9423ccf858d, type: 3}
|
||||
m_Color: {r: 0.9622642, g: 0.9069921, b: 0.063545614, a: 1}
|
||||
m_Sprite: {fileID: 6491965565530788803, guid: 7eac2cc728cf1a54f962afb35703891e, type: 3}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
m_DrawMode: 0
|
||||
@@ -152,7 +152,7 @@ BoxCollider2D:
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
oldSize: {x: 1.8138462, y: 1.843077}
|
||||
oldSize: {x: 3.9466667, y: 3.5866666}
|
||||
newSize: {x: 1, y: 1}
|
||||
adaptiveTilingThreshold: 0.5
|
||||
drawMode: 0
|
||||
|
||||
@@ -194,9 +194,9 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2801366364622282508, guid: 53891195a0ed1e84490e5a1b49760252, type: 3}
|
||||
m_Sprite: {fileID: 4535865847468002376, guid: 6aa747c9218e809439e10ede0685b401, type: 3}
|
||||
m_Color: {r: 0.8274511, g: 0.8274511, b: 0.8274511, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
@@ -263,7 +263,7 @@ BoxCollider2D:
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
oldSize: {x: 1.6471429, y: 1.7428571}
|
||||
oldSize: {x: 16.22, y: 10.8}
|
||||
newSize: {x: 1, y: 1}
|
||||
adaptiveTilingThreshold: 0.5
|
||||
drawMode: 0
|
||||
|
||||
@@ -645,7 +645,7 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2801366364622282508, guid: 8d11281e09df9ae47951c737bac19189, type: 3}
|
||||
m_Color: {r: 0.8274511, g: 0.8274511, b: 0.8274511, a: 1}
|
||||
|
||||
@@ -37,8 +37,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -20, y: -0.36}
|
||||
m_SizeDelta: {x: 2.1285, y: 1.0159}
|
||||
m_AnchoredPosition: {x: -19.849152, y: -0.30836427}
|
||||
m_SizeDelta: {x: 2.2826, y: 1.0159}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!212 &1626548957784810666
|
||||
SpriteRenderer:
|
||||
@@ -87,9 +87,9 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2475969391606739167, guid: 50ec29d5e415278408ddf08fae2f56a8, type: 3}
|
||||
m_Sprite: {fileID: -2103617111491369785, guid: 73513117b87e55849b59b2a21a09cc71, type: 3}
|
||||
m_Color: {r: 0.8274511, g: 0.8274511, b: 0.8274511, a: 1}
|
||||
m_FlipX: 0
|
||||
m_FlipY: 0
|
||||
@@ -156,7 +156,7 @@ BoxCollider2D:
|
||||
m_SpriteTilingProperty:
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
oldSize: {x: 1.6157143, y: 1.72}
|
||||
oldSize: {x: 12, y: 10.68}
|
||||
newSize: {x: 1, y: 1}
|
||||
adaptiveTilingThreshold: 0.5
|
||||
drawMode: 0
|
||||
@@ -356,7 +356,7 @@ MonoBehaviour:
|
||||
m_FallbackScreenDPI: 96
|
||||
m_DefaultSpriteDPI: 96
|
||||
m_DynamicPixelsPerUnit: 100
|
||||
m_PresetInfoIsWorld: 1
|
||||
m_PresetInfoIsWorld: 0
|
||||
--- !u!114 &6507110458437413086
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -683,7 +683,7 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: 2420144614108001737, guid: be1f4bd6f5da7444faa44a2e1e786511, type: 3}
|
||||
m_Color: {r: 0.9607844, g: 0.9607844, b: 0.9607844, a: 1}
|
||||
|
||||
@@ -395,7 +395,7 @@ SpriteRenderer:
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_SortingOrder: 2
|
||||
m_MaskInteraction: 0
|
||||
m_Sprite: {fileID: -6104705389903059128, guid: 0b3398c71c26e0b4596b76da3680f310, type: 3}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
|
||||
+11561
-143
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baa48fafda29f4247b2910dc9622c97a
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -16,7 +16,7 @@ public class CameraController : MonoBehaviour
|
||||
void Update()
|
||||
{
|
||||
// --- Перемещение по зажатой левой кнопке мыши ---
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
if (Input.GetMouseButtonDown(0) && !UIBlocker.IsPointerOverUI()) // не тащим камеру, когда листаем панель
|
||||
{
|
||||
_lastMousePosition = Input.mousePosition;
|
||||
_isDragging = true;
|
||||
@@ -39,7 +39,7 @@ public class CameraController : MonoBehaviour
|
||||
|
||||
// --- Зум по колёсику мыши ---
|
||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (scroll != 0f)
|
||||
if (scroll != 0f && !UIBlocker.IsPointerOverUI())
|
||||
{
|
||||
Camera cam = GetComponent<Camera>();
|
||||
float targetSize = cam.orthographicSize - scroll * zoomSpeed;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using UnityEngine;
|
||||
|
||||
// КАМЕРА ГОРОДА (как в мобильных стратегиях):
|
||||
// - перетаскивание одним пальцем / левой кнопкой мыши -- двигает карту
|
||||
// - щипок двумя пальцами / колёсико мыши -- приближение и отдаление (к точке под пальцем/курсором)
|
||||
// - камера не уезжает за границы карты (bounds)
|
||||
// - над интерфейсом (HUD, панели) ничего не делает
|
||||
// - без поворотов и наклонов: изометрия уже нарисована в спрайтах
|
||||
// Вешается на Main Camera вместо CameraController / CameraZoom / FlyCamera.
|
||||
[RequireComponent(typeof(Camera))]
|
||||
public class CityCamera : MonoBehaviour
|
||||
{
|
||||
[Header("Зум (orthographic size)")]
|
||||
public float minZoom = 3f;
|
||||
public float maxZoom = 14f;
|
||||
public float wheelZoomSpeed = 2.5f;
|
||||
public float zoomSmooth = 12f;
|
||||
|
||||
[Header("Границы карты (мировые координаты). Пусто = посчитать по объектам сцены")]
|
||||
public bool autoBounds = true;
|
||||
public Rect bounds = new Rect(-25, -15, 50, 30);
|
||||
[Tooltip("Запас за краем карты, чтобы край не прилипал к экрану")]
|
||||
public float boundsPadding = 4f;
|
||||
|
||||
[Header("Инерция после свайпа")]
|
||||
public float inertiaDamping = 6f;
|
||||
|
||||
Camera cam;
|
||||
float targetZoom;
|
||||
Vector3 lastWorld;
|
||||
bool dragging;
|
||||
Vector3 velocity;
|
||||
float lastPinchDist;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
cam = GetComponent<Camera>();
|
||||
cam.orthographic = true;
|
||||
transform.rotation = Quaternion.identity;
|
||||
transform.localScale = Vector3.one; // старый CameraZoom масштабировал камеру -- сбрасываем
|
||||
targetZoom = Mathf.Clamp(cam.orthographicSize, minZoom, maxZoom);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (autoBounds) CalculateBounds();
|
||||
ClampPosition();
|
||||
}
|
||||
|
||||
// Границы по всем спрайтам и тайлмапам сцены (кроме UI)
|
||||
public void CalculateBounds()
|
||||
{
|
||||
bool any = false;
|
||||
Bounds b = new Bounds();
|
||||
foreach (var r in FindObjectsByType<Renderer>(FindObjectsSortMode.None))
|
||||
{
|
||||
string tn = r.GetType().Name;
|
||||
if (tn != "SpriteRenderer" && tn != "TilemapRenderer") continue; // только карта и здания
|
||||
if (r.gameObject.layer == 5) continue; // UI
|
||||
if (r.GetComponentInParent<Canvas>() != null) continue; // всё, что внутри интерфейса
|
||||
if (!any) { b = r.bounds; any = true; } else b.Encapsulate(r.bounds);
|
||||
}
|
||||
if (any) bounds = new Rect(b.min.x, b.min.y, b.size.x, b.size.y);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
bool overUI = UIBlocker.IsPointerOverUI();
|
||||
|
||||
if (Input.touchCount >= 2) HandlePinch();
|
||||
else HandleDrag(overUI);
|
||||
|
||||
// колёсико мыши -- зум к курсору
|
||||
float wheel = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (Mathf.Abs(wheel) > 0.001f && !overUI)
|
||||
ZoomTowards(Input.mousePosition, targetZoom - wheel * wheelZoomSpeed * 4f);
|
||||
|
||||
// плавный зум
|
||||
if (Mathf.Abs(cam.orthographicSize - targetZoom) > 0.001f)
|
||||
{
|
||||
Vector3 before = cam.ScreenToWorldPoint(zoomAnchor);
|
||||
cam.orthographicSize = Mathf.Lerp(cam.orthographicSize, targetZoom, Time.unscaledDeltaTime * zoomSmooth);
|
||||
Vector3 after = cam.ScreenToWorldPoint(zoomAnchor);
|
||||
transform.position += before - after; // точка под пальцем остаётся на месте
|
||||
}
|
||||
|
||||
// инерция
|
||||
if (!dragging && velocity.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
transform.position += velocity * Time.unscaledDeltaTime;
|
||||
velocity = Vector3.Lerp(velocity, Vector3.zero, Time.unscaledDeltaTime * inertiaDamping);
|
||||
}
|
||||
|
||||
ClampPosition();
|
||||
}
|
||||
|
||||
Vector3 zoomAnchor;
|
||||
|
||||
void ZoomTowards(Vector3 screenPoint, float zoom)
|
||||
{
|
||||
zoomAnchor = screenPoint;
|
||||
targetZoom = Mathf.Clamp(zoom, minZoom, maxZoom);
|
||||
}
|
||||
|
||||
void HandleDrag(bool overUI)
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
dragging = !overUI; // начали тащить на интерфейсе -- карту не двигаем
|
||||
velocity = Vector3.zero;
|
||||
lastWorld = cam.ScreenToWorldPoint(Input.mousePosition);
|
||||
}
|
||||
if (Input.GetMouseButtonUp(0)) dragging = false;
|
||||
if (!dragging || !Input.GetMouseButton(0)) return;
|
||||
|
||||
Vector3 now = cam.ScreenToWorldPoint(Input.mousePosition);
|
||||
Vector3 delta = lastWorld - now;
|
||||
delta.z = 0;
|
||||
transform.position += delta;
|
||||
if (Time.unscaledDeltaTime > 0) velocity = Vector3.Lerp(velocity, delta / Time.unscaledDeltaTime, 0.5f);
|
||||
lastWorld = cam.ScreenToWorldPoint(Input.mousePosition); // после сдвига камеры
|
||||
}
|
||||
|
||||
void HandlePinch()
|
||||
{
|
||||
dragging = false;
|
||||
velocity = Vector3.zero;
|
||||
Touch a = Input.GetTouch(0), b = Input.GetTouch(1);
|
||||
float dist = Vector2.Distance(a.position, b.position);
|
||||
if (a.phase == TouchPhase.Began || b.phase == TouchPhase.Began || lastPinchDist <= 0f)
|
||||
{
|
||||
lastPinchDist = dist;
|
||||
return;
|
||||
}
|
||||
if (dist > 1f)
|
||||
{
|
||||
float ratio = lastPinchDist / dist;
|
||||
ZoomTowards((a.position + b.position) * 0.5f, targetZoom * ratio);
|
||||
}
|
||||
lastPinchDist = dist;
|
||||
if (a.phase == TouchPhase.Ended || b.phase == TouchPhase.Ended) lastPinchDist = 0f;
|
||||
}
|
||||
|
||||
void ClampPosition()
|
||||
{
|
||||
float halfH = cam.orthographicSize;
|
||||
float halfW = halfH * cam.aspect;
|
||||
Rect r = new Rect(bounds.xMin - boundsPadding, bounds.yMin - boundsPadding,
|
||||
bounds.width + boundsPadding * 2, bounds.height + boundsPadding * 2);
|
||||
Vector3 p = transform.position;
|
||||
p.x = r.width > halfW * 2 ? Mathf.Clamp(p.x, r.xMin + halfW, r.xMax - halfW) : r.center.x;
|
||||
p.y = r.height > halfH * 2 ? Mathf.Clamp(p.y, r.yMin + halfH, r.yMax - halfH) : r.center.y;
|
||||
transform.position = p;
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9530f2e8714d3746b25452c7fd56cd3
|
||||
@@ -0,0 +1,130 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Отвязывает HUD от камеры и ставит одну нормальную камеру.
|
||||
//
|
||||
// Было: HUD_Canvas в режиме World Space был дочерним объектом камеры, а CameraZoom масштабировал
|
||||
// саму камеру, чтобы HUD не уменьшался при зуме. Поэтому HUD ехал, дрожал и масштабировался криво.
|
||||
// Стало: HUD_Canvas -- Screen Space Overlay (всегда приклеен к экрану, камера на него не влияет),
|
||||
// на камере -- только CityCamera (перетаскивание, щипок, колёсико, границы карты).
|
||||
//
|
||||
// Работает в двух местах:
|
||||
// - при запуске игры (автоматически), если сцену ещё не исправили в редакторе;
|
||||
// - Tools -> Fix Camera & HUD (или Build HUD Panels) -- исправляет и сохраняет сцену навсегда.
|
||||
public static class HUDCameraSetup
|
||||
{
|
||||
// ОТКЛЮЧЕНО по просьбе команды: камера и HUD вернулись к прежней схеме (Tools -> Restore Old Camera & HUD).
|
||||
// Автоисправление при запуске больше не выполняется.
|
||||
static void AutoFix()
|
||||
{
|
||||
// и при каждой следующей загрузке города (возврат с карты мира)
|
||||
UnityEngine.SceneManagement.SceneManager.sceneLoaded += (s, m) => Apply(false);
|
||||
|
||||
int changes = Apply(false);
|
||||
if (changes > 0)
|
||||
Debug.Log("[КАМЕРА/HUD] Исправлено при запуске: " + changes +
|
||||
". Чтобы сохранить в сцене навсегда -- Tools -> Fix Camera & HUD");
|
||||
}
|
||||
|
||||
// editMode = true -- вызывается из редактора: лишние компоненты удаляются, а не выключаются
|
||||
public static int Apply(bool editMode)
|
||||
{
|
||||
int changes = 0;
|
||||
|
||||
// ---------- HUD ----------
|
||||
var hudGO = GameObject.Find("HUD_Canvas");
|
||||
if (hudGO != null)
|
||||
{
|
||||
var canvas = hudGO.GetComponent<Canvas>();
|
||||
var rt = (RectTransform)hudGO.transform;
|
||||
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
Vector2 designSize = rt.sizeDelta; // размер, под который дизайнер расставил HUD
|
||||
if (designSize.x < 100 || designSize.y < 100) designSize = new Vector2(1080, 1920);
|
||||
|
||||
if (hudGO.transform.parent != null) hudGO.transform.SetParent(null, false);
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 100;
|
||||
rt.localScale = Vector3.one;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
|
||||
var scaler = hudGO.GetComponent<CanvasScaler>();
|
||||
if (scaler == null) scaler = hudGO.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = designSize; // HUD выглядит так же, как его собрал дизайнер
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 1f; // как было в мире: высота HUD = высота экрана
|
||||
changes++;
|
||||
}
|
||||
// высота HUD = высота экрана (так HUD выглядел, пока был привязан к камере)
|
||||
var sc = hudGO.GetComponent<CanvasScaler>();
|
||||
if (sc != null && canvas != null && canvas.renderMode == RenderMode.ScreenSpaceOverlay && sc.matchWidthOrHeight != 1f)
|
||||
{ sc.matchWidthOrHeight = 1f; changes++; }
|
||||
changes += ConvertSpritesToImages(hudGO, editMode);
|
||||
changes += RemoveOrDisable(hudGO, "HUDWorldSpaceSync", editMode);
|
||||
}
|
||||
|
||||
// ---------- КАМЕРА ----------
|
||||
var cam = Camera.main;
|
||||
if (cam != null)
|
||||
{
|
||||
changes += RemoveOrDisable(cam.gameObject, "CameraZoom", editMode);
|
||||
changes += RemoveOrDisable(cam.gameObject, "CameraController", editMode);
|
||||
changes += RemoveOrDisable(cam.gameObject, "FlyCamera", editMode);
|
||||
if (cam.transform.localScale != Vector3.one) { cam.transform.localScale = Vector3.one; changes++; }
|
||||
if (cam.transform.rotation != Quaternion.identity) { cam.transform.rotation = Quaternion.identity; changes++; }
|
||||
if (cam.GetComponent<CityCamera>() == null) { cam.gameObject.AddComponent<CityCamera>(); changes++; }
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
// В экранном Canvas обычные SpriteRenderer не рисуются как часть интерфейса -- они улетают в мир
|
||||
// огромными картинками (отсюда белая "клетка" на весь экран). Переделываем их в UI Image того же размера.
|
||||
static int ConvertSpritesToImages(GameObject hud, bool editMode)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (var sr in hud.GetComponentsInChildren<SpriteRenderer>(true))
|
||||
{
|
||||
if (sr.sprite == null) { sr.enabled = false; continue; }
|
||||
var t = sr.transform;
|
||||
var parent = t.parent as RectTransform;
|
||||
if (parent == null) { sr.enabled = false; continue; }
|
||||
|
||||
var go = new GameObject(t.name + "_UI", typeof(RectTransform), typeof(Image));
|
||||
var rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
rt.SetSiblingIndex(t.GetSiblingIndex());
|
||||
rt.anchorMin = rt.anchorMax = parent.pivot; // localPosition считается от pivot родителя
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
Vector2 size = Vector2.Scale(sr.sprite.bounds.size, new Vector2(Mathf.Abs(t.localScale.x), Mathf.Abs(t.localScale.y)));
|
||||
rt.sizeDelta = size;
|
||||
rt.localPosition = t.localPosition + (Vector3)Vector2.Scale(sr.sprite.bounds.center, t.localScale);
|
||||
rt.localRotation = t.localRotation;
|
||||
var img = go.GetComponent<Image>();
|
||||
img.sprite = sr.sprite;
|
||||
img.color = sr.color;
|
||||
img.raycastTarget = false;
|
||||
img.preserveAspect = false;
|
||||
|
||||
if (editMode) Object.DestroyImmediate(t.gameObject);
|
||||
else t.gameObject.SetActive(false);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Компоненты ищем по имени класса: часть из них (FlyCamera, HUDWorldSpaceSync) в проекте уже нет
|
||||
static int RemoveOrDisable(GameObject go, string typeName, bool editMode)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (var mb in go.GetComponents<MonoBehaviour>())
|
||||
{
|
||||
if (mb == null || mb.GetType().Name != typeName) continue;
|
||||
if (editMode) Object.DestroyImmediate(mb);
|
||||
else if (mb.enabled) mb.enabled = false;
|
||||
else continue;
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 256165dd8ffe7dc4490c9e5b9796b782
|
||||
@@ -57,7 +57,7 @@ public class BuildManager : MonoBehaviour
|
||||
slotUnderPointer.SetHighlight(canAfford ? highlightValidColor : highlightInvalidColor);
|
||||
lastHighlightedSlot = slotUnderPointer;
|
||||
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
if (Input.GetMouseButtonDown(0) && !UIBlocker.IsPointerOverUI())
|
||||
{
|
||||
TryConfirmPlacement(slotUnderPointer, canAfford);
|
||||
}
|
||||
@@ -113,6 +113,7 @@ public class BuildManager : MonoBehaviour
|
||||
currentlyPlacing.PayCost();
|
||||
|
||||
GameObject built = Instantiate(currentlyPlacing.prefab, slot.transform.position, Quaternion.identity);
|
||||
if (!built.activeSelf) built.SetActive(true); // шаблон казарм хранится выключенным
|
||||
|
||||
PlacedBuildingId idMarker = built.AddComponent<PlacedBuildingId>();
|
||||
idMarker.buildingName = currentlyPlacing.buildingName;
|
||||
@@ -139,6 +140,39 @@ public class BuildManager : MonoBehaviour
|
||||
Debug.Log("[СТРОЙКА] Размещение отменено.");
|
||||
}
|
||||
|
||||
// ===== Новая схема стройки: тап по участку -> выбор здания (BuildModeController) =====
|
||||
|
||||
// Построить здание сразу на указанном участке. true -- получилось.
|
||||
public bool BuildOnSlot(BuildingData data, BuildSlot slot)
|
||||
{
|
||||
if (data == null || slot == null || slot.isOccupied) return false;
|
||||
if (!data.CanAfford()) return false;
|
||||
currentlyPlacing = data;
|
||||
TryConfirmPlacement(slot, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Участок под точкой экрана (с допуском, чтобы попадать с первого раза пальцем)
|
||||
public BuildSlot SlotAtScreen(Vector2 screenPos, float tolerance = 0.35f)
|
||||
{
|
||||
if (mainCam == null) mainCam = Camera.main;
|
||||
if (mainCam == null) return null;
|
||||
Vector3 w = mainCam.ScreenToWorldPoint(screenPos);
|
||||
Vector2 p = new Vector2(w.x, w.y);
|
||||
// Перебираем все участки и берём ближайший: работает и с триггер-коллайдерами, и без настроенного слоя
|
||||
BuildSlot best = null;
|
||||
float bestDist = tolerance;
|
||||
foreach (var slot in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None))
|
||||
{
|
||||
var col = slot.GetComponent<Collider2D>();
|
||||
Bounds b = col != null ? col.bounds : new Bounds(slot.transform.position, Vector3.one * 0.5f);
|
||||
b.center = new Vector3(b.center.x, b.center.y, 0f);
|
||||
float d = Mathf.Sqrt(b.SqrDistance(new Vector3(p.x, p.y, 0f)));
|
||||
if (d <= bestDist) { bestDist = d; best = slot; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Найти пустой (свободный) BuildSlot по названию -- используется при восстановлении сохранения
|
||||
public BuildSlot FindSlotAtPosition(Vector3 position, float tolerance = 0.05f)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,7 @@ public class TownHall : MonoBehaviour
|
||||
}
|
||||
void OnMouseDown()
|
||||
{
|
||||
if (UIBlocker.IsPointerOverUI()) return; // клик по панели HUD не должен улучшать Ратушу под ней
|
||||
Debug.Log($"[Ратуша] Клик! текущий уровень: {currentLevel}. Улучшение...");
|
||||
Upgrade();
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class ResourceCollector : MonoBehaviour
|
||||
|
||||
void OnMouseDown()
|
||||
{
|
||||
if (UIBlocker.IsPointerOverUI()) return; // клик по панели HUD не должен собирать ресурс под ней
|
||||
if (isRecharging)
|
||||
{
|
||||
Debug.Log($"[КЛИК] {gameObject.name}: уже идёт восстановление, ждём.");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Unity.Microsoft.GDK;
|
||||
using UnityEngine;
|
||||
|
||||
// Вешается на ЛЮБОЕ здание (лесопилка, каменоломня, шахта) -- настраиваешь
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1ddaca768e77fb4eb13bdb65900c14c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d34c90fa3d07d4c8f7b6cd6dcc3864
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// КАЗАРМЫ -- здание для найма войск.
|
||||
// - Без казарм нанимать войска нельзя.
|
||||
// - Каждые казармы дают +1 место в очереди обучения (1 казармы = 3 места, максимум 5).
|
||||
// - Нажатие на казармы открывает панель "Войска" (найм).
|
||||
// - Над зданием подпись: что сейчас обучается и сколько осталось.
|
||||
// Вешается на префаб Barracks (его создаёт Tools -> Build HUD Panels).
|
||||
public class Barracks : MonoBehaviour
|
||||
{
|
||||
public static readonly List<Barracks> All = new List<Barracks>();
|
||||
public static int Count { get { return All.Count; } }
|
||||
|
||||
// Размер очереди обучения в зависимости от числа казарм
|
||||
public static int QueueSize { get { return Count == 0 ? 0 : Mathf.Min(5, 2 + Count); } }
|
||||
|
||||
TextMeshPro label;
|
||||
float next;
|
||||
|
||||
void OnEnable() { if (!All.Contains(this)) All.Add(this); }
|
||||
void OnDisable() { All.Remove(this); }
|
||||
|
||||
void Start()
|
||||
{
|
||||
// подпись над зданием (обычный 3D-текст, не UI)
|
||||
var go = new GameObject("BarracksLabel");
|
||||
go.transform.SetParent(transform, false);
|
||||
var sr = GetComponent<SpriteRenderer>();
|
||||
float top = sr != null && sr.sprite != null ? sr.sprite.bounds.max.y : 1f;
|
||||
go.transform.localPosition = new Vector3(0, top + 0.15f, 0);
|
||||
label = go.AddComponent<TextMeshPro>();
|
||||
label.fontSize = 3f;
|
||||
label.alignment = TextAlignmentOptions.Center;
|
||||
label.rectTransform.sizeDelta = new Vector2(6f, 1.2f);
|
||||
label.color = Color.white;
|
||||
label.outlineWidth = 0.2f;
|
||||
label.outlineColor = Color.black;
|
||||
var r = label.GetComponent<MeshRenderer>();
|
||||
if (r != null) r.sortingOrder = (sr != null ? sr.sortingOrder : 0) + 10;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.unscaledTime < next) return;
|
||||
next = Time.unscaledTime + 0.5f;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Refresh()
|
||||
{
|
||||
if (label == null) return;
|
||||
var d = GameProgress.Instance.Data;
|
||||
if (d.training.Count == 0)
|
||||
{
|
||||
label.text = "Казармы\n<size=70%>нажмите, чтобы нанять</size>";
|
||||
return;
|
||||
}
|
||||
var o = d.training[0];
|
||||
label.text = "Казармы\n<size=70%>" + GameData.Troops[o.troopIndex].name + " ×" + o.count + " " +
|
||||
GameData.FormatTime(GameProgress.Instance.TrainingSecondsLeft(o)) +
|
||||
(d.training.Count > 1 ? " (+" + (d.training.Count - 1) + ")" : "") + "</size>";
|
||||
}
|
||||
|
||||
void OnMouseDown()
|
||||
{
|
||||
if (UIBlocker.IsPointerOverUI()) return;
|
||||
if (BuildModeController.Instance != null && BuildModeController.Instance.IsActive) return;
|
||||
if (UIPanelManager.Instance != null) UIPanelManager.Instance.Open("Troops");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96f3807928801df4a840181e38da1317
|
||||
@@ -0,0 +1,67 @@
|
||||
using UnityEngine;
|
||||
|
||||
// Добавляет "Казармы" в список стройки САМ при запуске сцены -- даже если никто не запускал
|
||||
// Tools -> Build HUD Panels. Если в BuildManager уже есть здание "Казармы" (из Data_Barracks.asset),
|
||||
// ничего не делает.
|
||||
//
|
||||
// Срабатывает после Awake всех объектов, но ДО Start -- то есть раньше, чем GameBootstrap загрузит
|
||||
// сохранение. Поэтому построенные казармы корректно восстанавливаются из save.json.
|
||||
public static class BarracksBootstrap
|
||||
{
|
||||
public const string BarracksName = "Казармы";
|
||||
static GameObject runtimeTemplate;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Init()
|
||||
{
|
||||
Ensure();
|
||||
// при каждом новом входе в город (например, возврат с карты мира) -- снова добавить в новый BuildManager
|
||||
UnityEngine.SceneManagement.SceneManager.sceneLoaded += (s, m) => Ensure();
|
||||
}
|
||||
|
||||
public static void Ensure()
|
||||
{
|
||||
var bm = BuildManager.Instance != null ? BuildManager.Instance : Object.FindAnyObjectByType<BuildManager>();
|
||||
if (bm == null || bm.allBuildings == null) return;
|
||||
|
||||
BuildingData template = null;
|
||||
foreach (var b in bm.allBuildings)
|
||||
{
|
||||
if (b == null) continue;
|
||||
if (b.buildingName == BarracksName) return; // уже есть
|
||||
if (template == null && b.prefab != null) template = b;
|
||||
}
|
||||
if (template == null) { Debug.LogWarning("[КАЗАРМЫ] Нет ни одного здания-шаблона в BuildManager"); return; }
|
||||
|
||||
// Префаб-шаблон в памяти: копия обычного здания без сбора ресурсов, с компонентом Barracks
|
||||
if (runtimeTemplate == null)
|
||||
{
|
||||
runtimeTemplate = Object.Instantiate(template.prefab);
|
||||
runtimeTemplate.name = "Barracks";
|
||||
runtimeTemplate.SetActive(false);
|
||||
Object.DontDestroyOnLoad(runtimeTemplate);
|
||||
foreach (var c in runtimeTemplate.GetComponents<ResourceCollector>()) Object.DestroyImmediate(c);
|
||||
foreach (var c in runtimeTemplate.GetComponents<ResourceProducer>()) Object.DestroyImmediate(c);
|
||||
foreach (var c in runtimeTemplate.GetComponents<BuildingUI>()) Object.DestroyImmediate(c);
|
||||
for (int i = runtimeTemplate.transform.childCount - 1; i >= 0; i--)
|
||||
Object.DestroyImmediate(runtimeTemplate.transform.GetChild(i).gameObject); // полоска запаса ресурса
|
||||
var sr = runtimeTemplate.GetComponent<SpriteRenderer>();
|
||||
if (sr != null) sr.color = new Color(1f, 0.55f, 0.55f); // пока нет арта -- отличаем цветом
|
||||
runtimeTemplate.AddComponent<Barracks>();
|
||||
}
|
||||
|
||||
var data = ScriptableObject.CreateInstance<BuildingData>();
|
||||
data.name = "Data_Barracks_Runtime";
|
||||
data.buildingName = BarracksName;
|
||||
data.icon = template.icon;
|
||||
data.prefab = runtimeTemplate;
|
||||
data.requiredTownHallLevel = 1;
|
||||
data.cost = new[]
|
||||
{
|
||||
new ResourceCost { type = ResourceType.Wood, amount = 300 },
|
||||
new ResourceCost { type = ResourceType.Stone, amount = 200 },
|
||||
};
|
||||
bm.allBuildings.Add(data);
|
||||
Debug.Log("[КАЗАРМЫ] Добавлены в список стройки");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27c12847dd0a97744aab16c30fab3f66
|
||||
@@ -0,0 +1,234 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// НОВАЯ СХЕМА СТРОЙКИ (как в Rise of Kingdoms):
|
||||
// 1. Кнопка "СТРОЙКА" включает режим строительства -- появляются свободные участки и подсказка сверху.
|
||||
// 2. Тап по свободному участку -> окно со зданиями (цена, что доступно по уровню Ратуши).
|
||||
// 3. "Построить" -> здание встаёт на участок. Режим остаётся включённым, можно строить дальше.
|
||||
// 4. Кнопка "ВЫЙТИ" (та же кнопка или в подсказке) -- выключает режим, участки снова прячутся.
|
||||
// Вне режима пустые участки не видны и не мешают, при запуске игры ничего не открывается.
|
||||
//
|
||||
// Вешается билдером (Tools -> Build HUD Panels) на HUD_Canvas, кнопка Nav_Build вызывает ToggleMode().
|
||||
public class BuildModeController : MonoBehaviour
|
||||
{
|
||||
public static BuildModeController Instance { get; private set; }
|
||||
|
||||
[Tooltip("Кнопка HUD, которая включает/выключает режим (надпись меняется сама)")]
|
||||
public Button modeButton;
|
||||
[Tooltip("Старое меню стройки -- прячем, оно больше не используется")]
|
||||
public GameObject legacyBuildMenu;
|
||||
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
GameObject banner;
|
||||
GameObject chooser;
|
||||
Vector2 downPos;
|
||||
bool downOverUI;
|
||||
float nextVisualRefresh;
|
||||
|
||||
static readonly Color SlotColor = new Color(0.3f, 1f, 0.4f, 0.75f);
|
||||
|
||||
void Awake() { Instance = this; }
|
||||
void OnDestroy() { if (Instance == this) Instance = null; }
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (legacyBuildMenu != null) legacyBuildMenu.SetActive(false);
|
||||
SetMode(false);
|
||||
}
|
||||
|
||||
public void ToggleMode() { SetMode(!IsActive); }
|
||||
|
||||
public void SetMode(bool active)
|
||||
{
|
||||
IsActive = active;
|
||||
CloseChooser();
|
||||
ApplySlotVisuals();
|
||||
UpdateButtonLabel();
|
||||
|
||||
if (banner != null) { Destroy(banner); banner = null; }
|
||||
if (active)
|
||||
{
|
||||
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
|
||||
banner = CreateBanner();
|
||||
if (FreeSlots().Count == 0) UIKit.Toast("Свободных участков нет. Улучшайте Ратушу и сносите старое.");
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.unscaledTime >= nextVisualRefresh)
|
||||
{
|
||||
nextVisualRefresh = Time.unscaledTime + 0.3f;
|
||||
ApplySlotVisuals(); // участки могли освободиться/заняться (снос, загрузка сохранения)
|
||||
}
|
||||
if (!IsActive) return;
|
||||
// открыли любую панель HUD -- выходим из режима стройки
|
||||
if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") { SetMode(false); return; }
|
||||
if (chooser != null) return;
|
||||
|
||||
// Тап = нажали и отпустили почти на том же месте (чтобы перетаскивание камеры не считалось тапом)
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
downPos = Input.mousePosition;
|
||||
downOverUI = UIBlocker.IsPointerOverUI();
|
||||
}
|
||||
if (Input.GetMouseButtonUp(0))
|
||||
{
|
||||
Vector2 up = Input.mousePosition;
|
||||
if (downOverUI || UIBlocker.IsPointerOverUI() || (up - downPos).magnitude > 25f) return;
|
||||
HandleTap(up);
|
||||
}
|
||||
}
|
||||
|
||||
void HandleTap(Vector2 screenPos)
|
||||
{
|
||||
if (BuildManager.Instance == null) return;
|
||||
var slot = BuildManager.Instance.SlotAtScreen(screenPos);
|
||||
if (slot == null) return;
|
||||
if (slot.isOccupied)
|
||||
{
|
||||
var id = slot.occupyingBuilding != null ? slot.occupyingBuilding.GetComponent<PlacedBuildingId>() : null;
|
||||
UIKit.Toast("Участок занят" + (id != null ? ": " + id.buildingName : ""));
|
||||
return;
|
||||
}
|
||||
OpenChooser(slot);
|
||||
}
|
||||
|
||||
// ---------------- участки ----------------
|
||||
|
||||
List<BuildSlot> FreeSlots()
|
||||
{
|
||||
var list = new List<BuildSlot>();
|
||||
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None)) if (!s.isOccupied) list.Add(s);
|
||||
return list;
|
||||
}
|
||||
|
||||
void ApplySlotVisuals()
|
||||
{
|
||||
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None))
|
||||
{
|
||||
var sr = s.GetComponent<SpriteRenderer>();
|
||||
if (sr == null) continue;
|
||||
bool show = IsActive && !s.isOccupied;
|
||||
if (sr.enabled != show) sr.enabled = show;
|
||||
if (show) sr.color = SlotColor;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- окно выбора здания ----------------
|
||||
|
||||
void OpenChooser(BuildSlot slot)
|
||||
{
|
||||
CloseChooser();
|
||||
var bm = BuildManager.Instance;
|
||||
GameObject root;
|
||||
var card = UIKit.Modal("Что построить?", out root);
|
||||
chooser = root;
|
||||
|
||||
// окно фиксированного размера с прокруткой, чтобы влезало на любом экране
|
||||
var fit = card.GetComponent<ContentSizeFitter>();
|
||||
if (fit != null) DestroyImmediate(fit);
|
||||
card.anchorMin = new Vector2(0.08f, 0.08f); card.anchorMax = new Vector2(0.92f, 0.92f);
|
||||
card.offsetMin = Vector2.zero; card.offsetMax = Vector2.zero;
|
||||
|
||||
int th = TownHall.Instance != null ? TownHall.Instance.currentLevel : 1;
|
||||
UIKit.Text(card, "Участок: " + slot.name + " • Ратуша " + th + " ур.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
|
||||
var holder = UIKit.Make("ListHolder", card);
|
||||
UIKit.Size(holder, -1, -1).flexibleHeight = 1;
|
||||
ScrollRect scroll;
|
||||
var list = UIKit.ScrollList(holder, out scroll, 10);
|
||||
|
||||
// доступные сверху, закрытые по уровню Ратуши -- ниже
|
||||
var all = new List<BuildingData>(bm.allBuildings);
|
||||
all.RemoveAll(b => b == null);
|
||||
all.Sort((a, b) => a.requiredTownHallLevel.CompareTo(b.requiredTownHallLevel));
|
||||
|
||||
foreach (var data in all)
|
||||
{
|
||||
var d = data;
|
||||
bool locked = d.requiredTownHallLevel > th;
|
||||
bool afford = !locked && d.CanAfford();
|
||||
|
||||
var row = UIKit.Row(list, 120, 14, locked ? UIKit.Theme.CardDark : UIKit.Theme.Card, 10);
|
||||
var icon = UIKit.Box(row, d.icon != null ? Color.white : UIKit.Theme.Gray);
|
||||
if (d.icon != null) { icon.sprite = d.icon; icon.preserveAspect = true; }
|
||||
UIKit.Size(icon, 100, 100);
|
||||
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 4);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
UIKit.Text(info, d.buildingName, 30, locked ? UIKit.Theme.TextDim : UIKit.Theme.Text).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, locked ? "Откроется на Ратуше " + d.requiredTownHallLevel + " уровня" : CostText(d), 24,
|
||||
locked ? UIKit.Theme.Gold : UIKit.Theme.TextDim);
|
||||
|
||||
string label = locked ? "Закрыто" : (afford ? "Построить" : "Мало ресурсов");
|
||||
var b = UIKit.Button(row, label, afford ? UIKit.Theme.Green : UIKit.Theme.Gray, () =>
|
||||
{
|
||||
if (BuildManager.Instance.BuildOnSlot(d, slot))
|
||||
{
|
||||
UIKit.Toast("Построено: " + d.buildingName);
|
||||
CloseChooser();
|
||||
ApplySlotVisuals();
|
||||
if (FreeSlots().Count == 0) SetMode(false);
|
||||
}
|
||||
else UIKit.Toast(GameProgress.Instance.MissingText(ToRewards(d.cost)));
|
||||
}, 230, 90, 26);
|
||||
b.interactable = !locked;
|
||||
}
|
||||
|
||||
UIKit.Button(card, "Отмена", UIKit.Theme.Gray, CloseChooser, -1, 90, 30);
|
||||
}
|
||||
|
||||
void CloseChooser()
|
||||
{
|
||||
if (chooser != null) Destroy(chooser);
|
||||
chooser = null;
|
||||
}
|
||||
|
||||
static RewardItem[] ToRewards(ResourceCost[] cost)
|
||||
{
|
||||
if (cost == null) return new RewardItem[0];
|
||||
var r = new RewardItem[cost.Length];
|
||||
for (int i = 0; i < cost.Length; i++) r[i] = new RewardItem(cost[i].type, Mathf.CeilToInt(cost[i].amount));
|
||||
return r;
|
||||
}
|
||||
|
||||
// Цена по-русски, красным -- чего не хватает
|
||||
static string CostText(BuildingData d)
|
||||
{
|
||||
if (d.cost == null || d.cost.Length == 0) return "Бесплатно";
|
||||
var parts = new List<string>();
|
||||
foreach (var c in d.cost)
|
||||
{
|
||||
bool ok = ResourceManager.Instance != null && ResourceManager.Instance.HasEnough(c.type, c.amount);
|
||||
parts.Add("<color=" + (ok ? "#9be59b" : "#ff7a7a") + ">" + GameData.Short(c.amount) + "</color> " + GameData.ResName(c.type));
|
||||
}
|
||||
return string.Join(", ", parts.ToArray());
|
||||
}
|
||||
|
||||
// ---------------- подсказка сверху и кнопка ----------------
|
||||
|
||||
GameObject CreateBanner()
|
||||
{
|
||||
// полоска сверху экрана (не модальная: остальной экран остаётся кликабельным)
|
||||
var bar = UIKit.Row(UIKit.Overlay, -1, 16, new Color(0.1f, 0.25f, 0.12f, 0.92f), 14);
|
||||
bar.name = "BuildModeBanner";
|
||||
bar.anchorMin = new Vector2(0.15f, 1); bar.anchorMax = new Vector2(0.85f, 1);
|
||||
bar.pivot = new Vector2(0.5f, 1);
|
||||
bar.sizeDelta = new Vector2(0, 110); bar.anchoredPosition = new Vector2(0, -150);
|
||||
var t = UIKit.Text(bar, "РЕЖИМ СТРОЙКИ: нажмите на зелёный участок", 30, Color.white, TextAlignmentOptions.Left);
|
||||
UIKit.Size(t, -1, -1, 1);
|
||||
UIKit.Button(bar, "Выйти", UIKit.Theme.Red, () => SetMode(false), 180, 80, 28);
|
||||
return bar.gameObject;
|
||||
}
|
||||
|
||||
void UpdateButtonLabel()
|
||||
{
|
||||
if (modeButton == null) return;
|
||||
var t = modeButton.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (t != null) t.text = IsActive ? "ГОТОВО" : "СТРОЙКА";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6e7cd0571b63f04088968696ac03a6a
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20c276885f20df54494e440a5e240f50
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// ВСЕ ИГРОВЫЕ ТАБЛИЦЫ В ОДНОМ МЕСТЕ: задания, войска, награды, магазин, событие.
|
||||
// Баланс правится здесь -- числа, названия, награды. Логика лежит в GameProgress и панелях.
|
||||
|
||||
[Serializable]
|
||||
public class RewardItem
|
||||
{
|
||||
public ResourceType type;
|
||||
public int amount;
|
||||
public RewardItem() { }
|
||||
public RewardItem(ResourceType type, int amount) { this.type = type; this.amount = amount; }
|
||||
}
|
||||
|
||||
public enum QuestKind
|
||||
{
|
||||
TownHallLevel, // улучшить Ратушу до уровня N
|
||||
BuildingsBuilt, // построить N зданий
|
||||
Collect, // собрать N ресурса
|
||||
BattlesWon, // выиграть N боёв
|
||||
TroopsTrained, // обучить N воинов
|
||||
HeroLevel, // герой N уровня
|
||||
ArenaFights, // N боёв на арене
|
||||
PlayerLevel, // уровень игрока N
|
||||
Login, // зайти в игру (ежедневное)
|
||||
WorldMarches, // отправить N походов на глобальной карте
|
||||
WorldWins, // победить N раз на глобальной карте
|
||||
BarracksBuilt // построить N казарм
|
||||
}
|
||||
|
||||
public class QuestDef
|
||||
{
|
||||
public string id;
|
||||
public string title;
|
||||
public QuestKind kind;
|
||||
public ResourceType resource;
|
||||
public int target;
|
||||
public int xp;
|
||||
public RewardItem[] rewards;
|
||||
|
||||
public QuestDef(string id, string title, QuestKind kind, int target, int xp, params RewardItem[] rewards)
|
||||
{
|
||||
this.id = id; this.title = title; this.kind = kind; this.target = target; this.xp = xp; this.rewards = rewards;
|
||||
}
|
||||
|
||||
public QuestDef Res(ResourceType r) { resource = r; return this; }
|
||||
}
|
||||
|
||||
public class TroopDef
|
||||
{
|
||||
public string name;
|
||||
public string description;
|
||||
public int power; // сила одного воина
|
||||
public float secondsPerUnit; // время обучения одного
|
||||
public int requiredTownHall;
|
||||
public RewardItem[] cost; // цена одного воина
|
||||
public Color color;
|
||||
|
||||
public TroopDef(string name, string description, int power, float seconds, int th, Color color, params RewardItem[] cost)
|
||||
{
|
||||
this.name = name; this.description = description; this.power = power; secondsPerUnit = seconds;
|
||||
requiredTownHall = th; this.color = color; this.cost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShopItemDef
|
||||
{
|
||||
public string id;
|
||||
public string title;
|
||||
public RewardItem[] gives;
|
||||
public ResourceType priceType;
|
||||
public int price; // 0 = бесплатно
|
||||
public int dailyLimit; // 0 = без лимита
|
||||
public bool comingSoon; // покупка за реальные деньги -- пока недоступна
|
||||
public string priceLabel;
|
||||
|
||||
public ShopItemDef(string id, string title, ResourceType priceType, int price, int dailyLimit, params RewardItem[] gives)
|
||||
{
|
||||
this.id = id; this.title = title; this.priceType = priceType; this.price = price; this.dailyLimit = dailyLimit; this.gives = gives;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameData
|
||||
{
|
||||
// ---------- Названия ресурсов на русском ----------
|
||||
public static string ResName(ResourceType t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case ResourceType.Population: return "Люди";
|
||||
case ResourceType.Food: return "Еда";
|
||||
case ResourceType.Wood: return "Дерево";
|
||||
case ResourceType.Stone: return "Камень";
|
||||
case ResourceType.Iron: return "Железо";
|
||||
case ResourceType.Gold: return "Золото";
|
||||
case ResourceType.Gems: return "Кристаллы";
|
||||
case ResourceType.Power: return "Сила";
|
||||
}
|
||||
return t.ToString();
|
||||
}
|
||||
|
||||
public static Color ResColor(ResourceType t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case ResourceType.Food: return new Color(0.85f, 0.45f, 0.3f);
|
||||
case ResourceType.Wood: return new Color(0.6f, 0.4f, 0.2f);
|
||||
case ResourceType.Stone: return new Color(0.6f, 0.6f, 0.65f);
|
||||
case ResourceType.Iron: return new Color(0.45f, 0.55f, 0.7f);
|
||||
case ResourceType.Gold: return new Color(1f, 0.8f, 0.2f);
|
||||
case ResourceType.Gems: return new Color(0.4f, 0.85f, 1f);
|
||||
case ResourceType.Power: return new Color(1f, 0.4f, 0.3f);
|
||||
}
|
||||
return new Color(0.8f, 0.8f, 0.8f);
|
||||
}
|
||||
|
||||
static RewardItem R(ResourceType t, int a) { return new RewardItem(t, a); }
|
||||
|
||||
// ---------- Опыт игрока ----------
|
||||
public static int XpToNextLevel(int level) { return 100 + (level - 1) * 80; }
|
||||
public const int MaxPlayerLevel = 60;
|
||||
|
||||
// ---------- Основные задания (цепочка) ----------
|
||||
public static readonly List<QuestDef> MainQuests = new List<QuestDef>
|
||||
{
|
||||
new QuestDef("m_collect_wood_1", "Собрать 200 дерева", QuestKind.Collect, 200, 30, R(ResourceType.Gold, 200)).Res(ResourceType.Wood),
|
||||
new QuestDef("m_collect_food_1", "Собрать 200 еды", QuestKind.Collect, 200, 30, R(ResourceType.Gold, 200)).Res(ResourceType.Food),
|
||||
new QuestDef("m_build_1", "Построить первое здание", QuestKind.BuildingsBuilt, 1, 50, R(ResourceType.Wood, 300), R(ResourceType.Stone, 200)),
|
||||
new QuestDef("m_barracks", "Построить Казармы", QuestKind.BarracksBuilt, 1, 50, R(ResourceType.Food, 300), R(ResourceType.Wood, 200)),
|
||||
new QuestDef("m_train_10", "Обучить 10 воинов", QuestKind.TroopsTrained, 10, 50, R(ResourceType.Food, 400)),
|
||||
new QuestDef("m_battle_1", "Выиграть первый бой", QuestKind.BattlesWon, 1, 60, R(ResourceType.Gems, 30)),
|
||||
new QuestDef("m_world_send", "Отправить армию в поход на карте мира", QuestKind.WorldMarches, 1, 60, R(ResourceType.Food, 500)),
|
||||
new QuestDef("m_world_win", "Разграбить поселение на карте мира", QuestKind.WorldWins, 1, 100, R(ResourceType.Gems, 50), R(ResourceType.Gold, 500)),
|
||||
new QuestDef("m_hero_3", "Герой 3 уровня", QuestKind.HeroLevel, 3, 60, R(ResourceType.Gold, 500)),
|
||||
new QuestDef("m_th_2", "Ратуша 2 уровня", QuestKind.TownHallLevel, 2, 100, R(ResourceType.Gems, 50), R(ResourceType.Stone, 500)),
|
||||
new QuestDef("m_build_3", "Построить 3 здания", QuestKind.BuildingsBuilt, 3, 80, R(ResourceType.Wood, 600), R(ResourceType.Iron, 200)),
|
||||
new QuestDef("m_arena_1", "Сразиться на арене", QuestKind.ArenaFights, 1, 60, R(ResourceType.Gems, 20)),
|
||||
new QuestDef("m_collect_stone_1", "Собрать 1000 камня", QuestKind.Collect, 1000, 80, R(ResourceType.Gold, 800)).Res(ResourceType.Stone),
|
||||
new QuestDef("m_train_100", "Обучить 100 воинов", QuestKind.TroopsTrained, 100, 120, R(ResourceType.Food, 1500), R(ResourceType.Gems, 30)),
|
||||
new QuestDef("m_battle_5", "Выиграть 5 боёв", QuestKind.BattlesWon, 5, 150, R(ResourceType.Gems, 60)),
|
||||
new QuestDef("m_player_5", "Достигнуть 5 уровня", QuestKind.PlayerLevel, 5, 100, R(ResourceType.Gems, 100)),
|
||||
new QuestDef("m_th_3", "Ратуша 3 уровня", QuestKind.TownHallLevel, 3, 200, R(ResourceType.Gems, 100), R(ResourceType.Iron, 800)),
|
||||
new QuestDef("m_hero_10", "Герой 10 уровня", QuestKind.HeroLevel, 10, 200, R(ResourceType.Gold, 3000)),
|
||||
new QuestDef("m_battle_15", "Выиграть 15 боёв", QuestKind.BattlesWon, 15, 300, R(ResourceType.Gems, 150)),
|
||||
new QuestDef("m_th_5", "Ратуша 5 уровня", QuestKind.TownHallLevel, 5, 400, R(ResourceType.Gems, 250)),
|
||||
};
|
||||
|
||||
// ---------- Ежедневные задания (сбрасываются каждый день) ----------
|
||||
public static readonly List<QuestDef> DailyQuests = new List<QuestDef>
|
||||
{
|
||||
new QuestDef("d_login", "Зайти в игру", QuestKind.Login, 1, 10, R(ResourceType.Gold, 100)),
|
||||
new QuestDef("d_wood", "Собрать 300 дерева", QuestKind.Collect, 300, 20, R(ResourceType.Food, 200)).Res(ResourceType.Wood),
|
||||
new QuestDef("d_food", "Собрать 300 еды", QuestKind.Collect, 300, 20, R(ResourceType.Wood, 200)).Res(ResourceType.Food),
|
||||
new QuestDef("d_train", "Обучить 20 воинов", QuestKind.TroopsTrained, 20, 25, R(ResourceType.Iron, 150)),
|
||||
new QuestDef("d_battle", "Выиграть 1 бой", QuestKind.BattlesWon, 1, 30, R(ResourceType.Gems, 10)),
|
||||
new QuestDef("d_arena", "2 боя на арене", QuestKind.ArenaFights, 2, 30, R(ResourceType.Gems, 10)),
|
||||
new QuestDef("d_world", "Отправить поход на карте мира", QuestKind.WorldMarches, 1, 25, R(ResourceType.Gold, 200)),
|
||||
};
|
||||
|
||||
// ---------- Войска ----------
|
||||
public static readonly List<TroopDef> Troops = new List<TroopDef>
|
||||
{
|
||||
new TroopDef("Пехота", "Дешёвые и надёжные бойцы ближнего боя.", 4, 3f, 1, new Color(0.55f, 0.6f, 0.7f),
|
||||
R(ResourceType.Food, 20), R(ResourceType.Wood, 10)),
|
||||
new TroopDef("Лучники", "Бьют издалека, хороши против пехоты.", 5, 4f, 1, new Color(0.45f, 0.7f, 0.4f),
|
||||
R(ResourceType.Food, 15), R(ResourceType.Wood, 25)),
|
||||
new TroopDef("Кавалерия", "Быстрые и сильные всадники.", 9, 7f, 2, new Color(0.75f, 0.55f, 0.3f),
|
||||
R(ResourceType.Food, 40), R(ResourceType.Iron, 15)),
|
||||
new TroopDef("Осадные", "Катапульты: огромная сила, долгая сборка.", 16, 12f, 3, new Color(0.7f, 0.35f, 0.35f),
|
||||
R(ResourceType.Wood, 60), R(ResourceType.Stone, 40), R(ResourceType.Iron, 30)),
|
||||
};
|
||||
// Размер очереди обучения зависит от числа казарм: см. Barracks.QueueSize
|
||||
|
||||
// ---------- Герой ----------
|
||||
public static int HeroMaxLevel(int townHallLevel) { return Mathf.Min(50, Mathf.Max(1, townHallLevel) * 5); }
|
||||
public static int HeroAttack(int lvl) { return 10 + (lvl - 1) * 5; }
|
||||
public static int HeroDefense(int lvl) { return 8 + (lvl - 1) * 4; }
|
||||
public static int HeroArmyBonusPercent(int lvl) { return lvl * 2; }
|
||||
public static int HeroPower(int lvl) { return 60 + lvl * 40; }
|
||||
public static RewardItem[] HeroUpgradeCost(int lvl)
|
||||
{
|
||||
float m = Mathf.Pow(1.35f, lvl - 1);
|
||||
return new[] { R(ResourceType.Gold, Mathf.RoundToInt(100 * m)), R(ResourceType.Food, Mathf.RoundToInt(150 * m)) };
|
||||
}
|
||||
public static readonly int[] HeroSkillLevels = { 1, 5, 10, 20, 35 };
|
||||
public static readonly string[] HeroSkillNames =
|
||||
{
|
||||
"Боевой клич: +5% к силе армии",
|
||||
"Железная стена: +10% защиты войск",
|
||||
"Вдохновение: +10% к наградам за бой",
|
||||
"Стратег: потери в бою -20%",
|
||||
"Легенда: +15% к силе армии",
|
||||
};
|
||||
|
||||
// ---------- Кампания (вкладка "Бой") ----------
|
||||
public static readonly string[] EnemyNames =
|
||||
{
|
||||
"Лагерь разбойников", "Волчье логово", "Застава мародёров", "Гоблинский форт", "Тёмная роща",
|
||||
"Пиратская бухта", "Крепость орков", "Проклятые руины", "Логово тролля", "Цитадель некроманта"
|
||||
};
|
||||
public static string StageName(int stage) { return EnemyNames[(stage - 1) % EnemyNames.Length] + (stage > EnemyNames.Length ? " II" : ""); }
|
||||
public static int StageEnemyPower(int stage) { return Mathf.RoundToInt(80f * Mathf.Pow(1.22f, stage - 1)); }
|
||||
public static RewardItem[] StageRewards(int stage)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
R(ResourceType.Gold, 60 + stage * 25),
|
||||
R(ResourceType.Food, 80 + stage * 30),
|
||||
R(ResourceType.Iron, 20 + stage * 10),
|
||||
};
|
||||
}
|
||||
public static int StageXp(int stage) { return 20 + stage * 6; }
|
||||
public static int StageGems(int stage) { return stage % 5 == 0 ? 25 : 0; } // каждый 5-й этап -- кристаллы
|
||||
|
||||
// ---------- Арена ----------
|
||||
public const int ArenaMaxTickets = 5;
|
||||
public const int ArenaTicketMinutes = 20;
|
||||
public static readonly string[] BotNames =
|
||||
{
|
||||
"Ярослав", "Мирон", "Север", "Ведана", "Ратибор", "Злата", "Горыня", "Любава", "Святогор", "Огнеслав",
|
||||
"Ingvar", "Sigrun", "Bjorn", "Astrid", "Leif", "Ragnar", "Freya", "Ulf", "Helga", "Torvald"
|
||||
};
|
||||
public static string ArenaLeague(int rating)
|
||||
{
|
||||
if (rating < 1100) return "Бронза";
|
||||
if (rating < 1300) return "Серебро";
|
||||
if (rating < 1600) return "Золото";
|
||||
if (rating < 2000) return "Платина";
|
||||
return "Алмаз";
|
||||
}
|
||||
|
||||
// ---------- Ежедневная награда за вход (7 дней) ----------
|
||||
public static readonly RewardItem[][] LoginRewards =
|
||||
{
|
||||
new[] { R(ResourceType.Gold, 500) },
|
||||
new[] { R(ResourceType.Food, 1000) },
|
||||
new[] { R(ResourceType.Wood, 800), R(ResourceType.Stone, 500) },
|
||||
new[] { R(ResourceType.Gems, 50) },
|
||||
new[] { R(ResourceType.Iron, 600) },
|
||||
new[] { R(ResourceType.Gold, 2000) },
|
||||
new[] { R(ResourceType.Gems, 200) },
|
||||
};
|
||||
|
||||
// ---------- Магазин ----------
|
||||
public static readonly List<ShopItemDef> ShopItems = BuildShop();
|
||||
static List<ShopItemDef> BuildShop()
|
||||
{
|
||||
var list = new List<ShopItemDef>
|
||||
{
|
||||
new ShopItemDef("free_chest", "Бесплатный сундук (раз в день)", ResourceType.Gems, 0, 1),
|
||||
new ShopItemDef("wood_1k", "Пакет дерева", ResourceType.Gems, 50, 5, R(ResourceType.Wood, 1000)),
|
||||
new ShopItemDef("stone_1k", "Пакет камня", ResourceType.Gems, 50, 5, R(ResourceType.Stone, 1000)),
|
||||
new ShopItemDef("food_15", "Пакет еды", ResourceType.Gems, 50, 5, R(ResourceType.Food, 1500)),
|
||||
new ShopItemDef("iron_500", "Пакет железа", ResourceType.Gems, 60, 5, R(ResourceType.Iron, 500)),
|
||||
new ShopItemDef("gold_1k", "Мешок золота", ResourceType.Gems, 80, 3, R(ResourceType.Gold, 1000)),
|
||||
new ShopItemDef("gems_10", "Обмен золота на кристаллы", ResourceType.Gold, 1000, 3, R(ResourceType.Gems, 10)),
|
||||
};
|
||||
var p1 = new ShopItemDef("iap_small", "Горсть кристаллов", ResourceType.Gems, 0, 0, R(ResourceType.Gems, 500));
|
||||
p1.comingSoon = true; p1.priceLabel = "99 руб.";
|
||||
var p2 = new ShopItemDef("iap_big", "Сундук кристаллов", ResourceType.Gems, 0, 0, R(ResourceType.Gems, 3000));
|
||||
p2.comingSoon = true; p2.priceLabel = "499 руб.";
|
||||
list.Add(p1); list.Add(p2);
|
||||
return list;
|
||||
}
|
||||
// Содержимое бесплатного сундука (выбирается случайно)
|
||||
public static readonly RewardItem[] FreeChestPool =
|
||||
{
|
||||
R(ResourceType.Wood, 500), R(ResourceType.Stone, 400), R(ResourceType.Food, 600),
|
||||
R(ResourceType.Iron, 250), R(ResourceType.Gold, 400), R(ResourceType.Gems, 15),
|
||||
};
|
||||
|
||||
// ---------- Еженедельное событие ----------
|
||||
public const string EventTitle = "Сбор войск";
|
||||
public const string EventDescription =
|
||||
"Очки события даются за: победу в бою (+10), разграбление на карте мира (+15), бой на арене (+5), выполненное задание (+20), каждые 5 обученных воинов (+1).";
|
||||
public static readonly int[] EventTiers = { 50, 150, 300, 500, 800 };
|
||||
public static readonly RewardItem[][] EventTierRewards =
|
||||
{
|
||||
new[] { R(ResourceType.Gold, 500) },
|
||||
new[] { R(ResourceType.Food, 1500), R(ResourceType.Wood, 1000) },
|
||||
new[] { R(ResourceType.Gems, 50) },
|
||||
new[] { R(ResourceType.Iron, 1500), R(ResourceType.Gold, 2000) },
|
||||
new[] { R(ResourceType.Gems, 200) },
|
||||
};
|
||||
|
||||
// ---------- Текст наград ----------
|
||||
public static string RewardsText(RewardItem[] rewards)
|
||||
{
|
||||
if (rewards == null || rewards.Length == 0) return "";
|
||||
var parts = new List<string>();
|
||||
foreach (var r in rewards) parts.Add(Short(r.amount) + " " + ResName(r.type));
|
||||
return string.Join(", ", parts.ToArray());
|
||||
}
|
||||
|
||||
public static string Short(float value)
|
||||
{
|
||||
if (value >= 1000000f) return (value / 1000000f).ToString("0.#") + "M";
|
||||
if (value >= 10000f) return (value / 1000f).ToString("0.#") + "K";
|
||||
return Mathf.FloorToInt(value).ToString();
|
||||
}
|
||||
|
||||
public static string FormatTime(double seconds)
|
||||
{
|
||||
if (seconds < 0) seconds = 0;
|
||||
TimeSpan t = TimeSpan.FromSeconds(Math.Ceiling(seconds));
|
||||
if (t.TotalDays >= 1) return string.Format("{0}д {1}ч", (int)t.TotalDays, t.Hours);
|
||||
if (t.TotalHours >= 1) return string.Format("{0}ч {1:00}м", (int)t.TotalHours, t.Minutes);
|
||||
return string.Format("{0:00}:{1:00}", t.Minutes, t.Seconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 203996cec7ec1514fb39e444697880ec
|
||||
@@ -0,0 +1,930 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
// "МОЗГ" всех панелей HUD: профиль, задания, почта, магазин, герой, войска, бой, арена, событие.
|
||||
// Панели только РИСУЮТ и вызывают методы отсюда -- вся логика и сохранение здесь.
|
||||
//
|
||||
// Создаётся сам при запуске сцены (ничего вешать руками не нужно).
|
||||
// Сохраняется в persistentDataPath/progress.json -- рядом с save.json от SaveManager.
|
||||
public class GameProgress : MonoBehaviour
|
||||
{
|
||||
static GameProgress instance;
|
||||
public static GameProgress Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = FindAnyObjectByType<GameProgress>();
|
||||
if (instance == null) instance = new GameObject("GameProgress").AddComponent<GameProgress>();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void AutoCreate() { var _ = Instance; }
|
||||
|
||||
public ProgressData Data { get; private set; }
|
||||
|
||||
// Любое изменение прогресса -> панели перерисовываются
|
||||
public event Action OnChanged;
|
||||
|
||||
string SavePath { get { return Path.Combine(Application.persistentDataPath, "progress.json"); } }
|
||||
|
||||
bool dirty;
|
||||
float saveAt = -1f;
|
||||
float nextTick;
|
||||
int suppressTracking; // >0 -- выдаём награду, не считаем это "сбором"
|
||||
float trackingStartsAt; // первые секунды после старта не считаем (идёт загрузка сохранения)
|
||||
bool subscribed;
|
||||
readonly float[] lastValues = new float[8];
|
||||
int cachedBuildings;
|
||||
|
||||
static readonly DateTime Epoch = new DateTime(2024, 1, 1); // понедельник
|
||||
|
||||
// =====================================================================
|
||||
// ЖИЗНЕННЫЙ ЦИКЛ
|
||||
// =====================================================================
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this) { Destroy(gameObject); return; }
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Load();
|
||||
trackingStartsAt = Time.unscaledTime + 2f;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!subscribed) TrySubscribe();
|
||||
|
||||
if (Time.unscaledTime >= nextTick)
|
||||
{
|
||||
nextTick = Time.unscaledTime + 1f;
|
||||
EnsureDailyAndWeekly();
|
||||
ProcessTraining();
|
||||
WorldSystem.Process();
|
||||
UpdateBuildingsCount();
|
||||
UpdatePowerResource();
|
||||
}
|
||||
|
||||
if (saveAt > 0 && Time.unscaledTime >= saveAt) { saveAt = -1f; Save(); }
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
dirty = false;
|
||||
if (OnChanged != null) OnChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool paused) { if (paused) Save(); }
|
||||
void OnApplicationQuit() { Save(); }
|
||||
|
||||
// Сохранить без перерисовки панелей (для ползунков настроек)
|
||||
public void SaveSoon()
|
||||
{
|
||||
if (saveAt < 0) saveAt = Time.unscaledTime + 1.5f;
|
||||
}
|
||||
|
||||
public void MarkChanged()
|
||||
{
|
||||
dirty = true;
|
||||
if (saveAt < 0) saveAt = Time.unscaledTime + 1.5f;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// СОХРАНЕНИЕ
|
||||
// =====================================================================
|
||||
|
||||
void Load()
|
||||
{
|
||||
Data = null;
|
||||
try
|
||||
{
|
||||
if (File.Exists(SavePath)) Data = JsonUtility.FromJson<ProgressData>(File.ReadAllText(SavePath));
|
||||
}
|
||||
catch (Exception e) { Debug.LogWarning("[ПРОГРЕСС] Не удалось прочитать progress.json: " + e.Message); }
|
||||
|
||||
if (Data == null) Data = new ProgressData();
|
||||
FixData();
|
||||
EnsureDailyAndWeekly();
|
||||
|
||||
// Новый день -> считаем день в игре
|
||||
int today = Today;
|
||||
if (Data.lastLoginDay != today) { Data.lastLoginDay = today; Data.daysPlayed++; }
|
||||
|
||||
if (!Data.starterMailSent) SendStarterMail();
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
// Защита от старых/кривых файлов: массивы нужной длины, списки не null
|
||||
void FixData()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Data.playerId)) Data.playerId = UnityEngine.Random.Range(10000000, 99999999).ToString();
|
||||
if (Data.firstLaunchTicks == 0) Data.firstLaunchTicks = DateTime.UtcNow.Ticks;
|
||||
if (Data.collected == null || Data.collected.Length < 8) Data.collected = Resize(Data.collected, 8);
|
||||
if (Data.dailyCollected == null || Data.dailyCollected.Length < 8) Data.dailyCollected = Resize(Data.dailyCollected, 8);
|
||||
if (Data.troops == null || Data.troops.Length < GameData.Troops.Count)
|
||||
{
|
||||
int[] t = new int[GameData.Troops.Count];
|
||||
if (Data.troops != null) Array.Copy(Data.troops, t, Mathf.Min(Data.troops.Length, t.Length));
|
||||
Data.troops = t;
|
||||
}
|
||||
if (Data.claimedQuests == null) Data.claimedQuests = new List<string>();
|
||||
if (Data.claimedDaily == null) Data.claimedDaily = new List<string>();
|
||||
if (Data.mail == null) Data.mail = new List<MailItem>();
|
||||
if (Data.shopIds == null) Data.shopIds = new List<string>();
|
||||
if (Data.shopCounts == null) Data.shopCounts = new List<int>();
|
||||
if (Data.training == null) Data.training = new List<TrainingOrder>();
|
||||
if (Data.arenaOpponents == null) Data.arenaOpponents = new List<ArenaOpponent>();
|
||||
if (Data.eventClaimed == null) Data.eventClaimed = new List<int>();
|
||||
if (Data.settlements == null) Data.settlements = new List<SettlementState>();
|
||||
if (Data.marches == null) Data.marches = new List<WorldMarch>();
|
||||
if (Data.reports == null) Data.reports = new List<BattleReport>();
|
||||
if (Data.sashaMarches == null) Data.sashaMarches = new List<Bogatyri.World.MarchData>();
|
||||
if (Data.sashaMarchUnits == null) Data.sashaMarchUnits = new List<ArmyBridge.MarchUnits>();
|
||||
if (Data.level < 1) Data.level = 1;
|
||||
if (Data.heroLevel < 1) Data.heroLevel = 1;
|
||||
if (Data.battleStage < 1) Data.battleStage = 1;
|
||||
}
|
||||
|
||||
static float[] Resize(float[] src, int len)
|
||||
{
|
||||
float[] r = new float[len];
|
||||
if (src != null) Array.Copy(src, r, Mathf.Min(src.Length, len));
|
||||
return r;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(SavePath, JsonUtility.ToJson(Data, true));
|
||||
}
|
||||
catch (Exception e) { Debug.LogWarning("[ПРОГРЕСС] Не удалось сохранить: " + e.Message); }
|
||||
}
|
||||
|
||||
// Полный сброс: прогресс, ресурсы, здания, Ратуша. Кнопка в Настройках.
|
||||
public void ResetAll()
|
||||
{
|
||||
if (File.Exists(SavePath)) File.Delete(SavePath);
|
||||
if (SaveManager.Instance != null) SaveManager.Instance.DeleteSave();
|
||||
|
||||
// Здания: удаляем -- BuildingSlotLink сам освободит слоты
|
||||
foreach (var b in FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None)) Destroy(b.gameObject);
|
||||
if (TownHall.Instance != null) TownHall.Instance.currentLevel = 1;
|
||||
|
||||
var rm = ResourceManager.Instance;
|
||||
if (rm != null)
|
||||
{
|
||||
suppressTracking++;
|
||||
rm.SetResource(ResourceType.Wood, rm.startWood);
|
||||
rm.SetResource(ResourceType.Stone, rm.startStone);
|
||||
rm.SetResource(ResourceType.Iron, rm.startIron);
|
||||
rm.SetResource(ResourceType.Food, rm.startFood);
|
||||
rm.SetResource(ResourceType.Gold, rm.startGold);
|
||||
rm.SetResource(ResourceType.Gems, rm.startGems);
|
||||
rm.SetResource(ResourceType.Population, rm.startPopulation);
|
||||
suppressTracking--;
|
||||
}
|
||||
|
||||
Data = new ProgressData();
|
||||
FixData();
|
||||
Data.lastLoginDay = Today; Data.daysPlayed = 1;
|
||||
EnsureDailyAndWeekly();
|
||||
SendStarterMail();
|
||||
ApplySettings();
|
||||
Save();
|
||||
MarkChanged();
|
||||
UIKit.Toast("Прогресс сброшен. Новая игра!");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ВРЕМЯ
|
||||
// =====================================================================
|
||||
|
||||
public static int Today { get { return (DateTime.Now.Date - Epoch).Days; } }
|
||||
public static int CurrentWeek { get { return Today / 7; } }
|
||||
public static double SecondsToNextDay { get { return (DateTime.Now.Date.AddDays(1) - DateTime.Now).TotalSeconds; } }
|
||||
public static double SecondsToWeekEnd
|
||||
{
|
||||
get { return (Epoch.AddDays((CurrentWeek + 1) * 7) - DateTime.Now).TotalSeconds; }
|
||||
}
|
||||
static long Now { get { return DateTime.UtcNow.Ticks; } }
|
||||
|
||||
void EnsureDailyAndWeekly()
|
||||
{
|
||||
int today = Today;
|
||||
if (Data.dailyDay != today)
|
||||
{
|
||||
Data.dailyDay = today;
|
||||
Data.claimedDaily.Clear();
|
||||
Data.dailyCollected = new float[8];
|
||||
Data.dailyBattles = 0; Data.dailyTroops = 0; Data.dailyArena = 0; Data.dailyMarches = 0;
|
||||
MarkChanged();
|
||||
}
|
||||
if (Data.shopDay != today)
|
||||
{
|
||||
Data.shopDay = today;
|
||||
Data.shopIds.Clear(); Data.shopCounts.Clear();
|
||||
}
|
||||
if (Data.eventWeek != CurrentWeek)
|
||||
{
|
||||
Data.eventWeek = CurrentWeek;
|
||||
Data.eventPoints = 0;
|
||||
Data.eventTroopRemainder = 0;
|
||||
Data.eventClaimed.Clear();
|
||||
MarkChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// РЕСУРСЫ И НАГРАДЫ
|
||||
// =====================================================================
|
||||
|
||||
public float Res(ResourceType t) { return ResourceManager.Instance != null ? ResourceManager.Instance.GetResource(t) : 0f; }
|
||||
|
||||
public bool CanAfford(RewardItem[] cost, int multiplier = 1)
|
||||
{
|
||||
if (ResourceManager.Instance == null) return false;
|
||||
foreach (var c in cost)
|
||||
if (!ResourceManager.Instance.HasEnough(c.type, (float)c.amount * multiplier)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public string MissingText(RewardItem[] cost, int multiplier = 1)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var c in cost)
|
||||
{
|
||||
float need = (float)c.amount * multiplier - Res(c.type);
|
||||
if (need > 0) parts.Add(GameData.Short(Mathf.Ceil(need)) + " " + GameData.ResName(c.type));
|
||||
}
|
||||
return parts.Count == 0 ? "" : "Не хватает: " + string.Join(", ", parts.ToArray());
|
||||
}
|
||||
|
||||
// Списать цену. false -- не хватило (ничего не списано + всплывашка)
|
||||
public bool Pay(RewardItem[] cost, int multiplier = 1)
|
||||
{
|
||||
if (!CanAfford(cost, multiplier))
|
||||
{
|
||||
UIKit.Toast(MissingText(cost, multiplier));
|
||||
return false;
|
||||
}
|
||||
foreach (var c in cost) ResourceManager.Instance.TrySpend(c.type, (float)c.amount * multiplier);
|
||||
MarkChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Grant(RewardItem[] rewards, int xp = 0, bool toast = true)
|
||||
{
|
||||
if (ResourceManager.Instance != null && rewards != null)
|
||||
{
|
||||
suppressTracking++;
|
||||
foreach (var r in rewards) ResourceManager.Instance.AddResource(r.type, r.amount);
|
||||
suppressTracking--;
|
||||
}
|
||||
if (xp > 0) AddXp(xp);
|
||||
if (toast)
|
||||
{
|
||||
string text = GameData.RewardsText(rewards);
|
||||
if (xp > 0) text += (text.Length > 0 ? ", " : "") + xp + " опыта";
|
||||
if (text.Length > 0) UIKit.Toast("Получено: " + text);
|
||||
}
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ОТСЛЕЖИВАНИЕ СБОРА РЕСУРСОВ (для заданий и статистики)
|
||||
// =====================================================================
|
||||
|
||||
void TrySubscribe()
|
||||
{
|
||||
var rm = ResourceManager.Instance;
|
||||
if (rm == null) return;
|
||||
rm.OnResourceChanged += HandleResourceChanged;
|
||||
foreach (ResourceType t in Enum.GetValues(typeof(ResourceType)))
|
||||
if ((int)t < lastValues.Length) lastValues[(int)t] = rm.GetResource(t);
|
||||
subscribed = true;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (subscribed && ResourceManager.Instance != null) ResourceManager.Instance.OnResourceChanged -= HandleResourceChanged;
|
||||
}
|
||||
|
||||
void HandleResourceChanged(ResourceType type, float value)
|
||||
{
|
||||
int i = (int)type;
|
||||
if (i < 0 || i >= lastValues.Length) return;
|
||||
float delta = value - lastValues[i];
|
||||
lastValues[i] = value;
|
||||
|
||||
if (type == ResourceType.Power) return;
|
||||
if (delta <= 0 || suppressTracking > 0 || Time.unscaledTime < trackingStartsAt) { MarkChanged(); return; }
|
||||
|
||||
Data.collected[i] += delta;
|
||||
Data.dailyCollected[i] += delta;
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
void UpdateBuildingsCount()
|
||||
{
|
||||
int n = FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None).Length;
|
||||
if (n != cachedBuildings) { cachedBuildings = n; MarkChanged(); }
|
||||
}
|
||||
|
||||
public int BuildingsCount { get { return cachedBuildings; } }
|
||||
public int TownHallLevel { get { return TownHall.Instance != null ? TownHall.Instance.currentLevel : 1; } }
|
||||
|
||||
// =====================================================================
|
||||
// ОПЫТ И УРОВЕНЬ
|
||||
// =====================================================================
|
||||
|
||||
public void AddXp(int amount)
|
||||
{
|
||||
if (Data.level >= GameData.MaxPlayerLevel) return;
|
||||
Data.xp += amount;
|
||||
while (Data.level < GameData.MaxPlayerLevel && Data.xp >= GameData.XpToNextLevel(Data.level))
|
||||
{
|
||||
Data.xp -= GameData.XpToNextLevel(Data.level);
|
||||
Data.level++;
|
||||
int gems = 10 * Data.level;
|
||||
SendMail("Новый уровень: " + Data.level + "!",
|
||||
"Поздравляем, командир! Ваша слава растёт. Примите подарок от совета королевства.",
|
||||
new RewardItem(ResourceType.Gems, gems), new RewardItem(ResourceType.Gold, 300 * Data.level));
|
||||
UIKit.Toast("Новый уровень: " + Data.level + "! Подарок в почте.");
|
||||
Vibrate();
|
||||
}
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// СИЛА
|
||||
// =====================================================================
|
||||
|
||||
public int TroopsPower
|
||||
{
|
||||
get
|
||||
{
|
||||
int p = 0;
|
||||
for (int i = 0; i < GameData.Troops.Count; i++) p += Data.troops[i] * GameData.Troops[i].power;
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
public float ArmyBonus
|
||||
{
|
||||
get
|
||||
{
|
||||
float b = GameData.HeroArmyBonusPercent(Data.heroLevel) / 100f;
|
||||
if (HasHeroSkill(0)) b += 0.05f;
|
||||
if (HasHeroSkill(4)) b += 0.15f;
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
// Сила армии в бою = войска * бонус героя + сила героя
|
||||
public int ArmyPower { get { return Mathf.RoundToInt(TroopsPower * (1f + ArmyBonus)) + GameData.HeroPower(Data.heroLevel); } }
|
||||
|
||||
public int TotalPower { get { return ArmyPower + TownHallLevel * 100 + BuildingsCount * 25; } }
|
||||
|
||||
void UpdatePowerResource()
|
||||
{
|
||||
var rm = ResourceManager.Instance;
|
||||
if (rm == null) return;
|
||||
int p = TotalPower;
|
||||
if (Mathf.RoundToInt(rm.GetResource(ResourceType.Power)) != p) rm.SetResource(ResourceType.Power, p);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ЗАДАНИЯ
|
||||
// =====================================================================
|
||||
|
||||
public int QuestProgress(QuestDef q, bool daily)
|
||||
{
|
||||
switch (q.kind)
|
||||
{
|
||||
case QuestKind.TownHallLevel: return TownHallLevel;
|
||||
case QuestKind.BuildingsBuilt: return BuildingsCount;
|
||||
case QuestKind.Collect: return Mathf.FloorToInt(daily ? Data.dailyCollected[(int)q.resource] : Data.collected[(int)q.resource]);
|
||||
case QuestKind.BattlesWon: return daily ? Data.dailyBattles : Data.battlesWon;
|
||||
case QuestKind.TroopsTrained: return daily ? Data.dailyTroops : Data.troopsTrained;
|
||||
case QuestKind.HeroLevel: return Data.heroLevel;
|
||||
case QuestKind.ArenaFights: return daily ? Data.dailyArena : Data.arenaFights;
|
||||
case QuestKind.PlayerLevel: return Data.level;
|
||||
case QuestKind.Login: return 1;
|
||||
case QuestKind.WorldMarches: return daily ? Data.dailyMarches : Data.marchesSent;
|
||||
case QuestKind.WorldWins: return Data.worldWins;
|
||||
case QuestKind.BarracksBuilt: return Barracks.Count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool IsQuestClaimed(QuestDef q, bool daily) { return (daily ? Data.claimedDaily : Data.claimedQuests).Contains(q.id); }
|
||||
public bool IsQuestDone(QuestDef q, bool daily) { return QuestProgress(q, daily) >= q.target; }
|
||||
|
||||
public void ClaimQuest(QuestDef q, bool daily)
|
||||
{
|
||||
if (IsQuestClaimed(q, daily) || !IsQuestDone(q, daily)) return;
|
||||
(daily ? Data.claimedDaily : Data.claimedQuests).Add(q.id);
|
||||
Data.questsDone++;
|
||||
AddEventPoints(20);
|
||||
Grant(q.rewards, q.xp);
|
||||
}
|
||||
|
||||
public int ClaimableQuestsCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
foreach (var q in GameData.MainQuests) if (!IsQuestClaimed(q, false) && IsQuestDone(q, false)) n++;
|
||||
foreach (var q in GameData.DailyQuests) if (!IsQuestClaimed(q, true) && IsQuestDone(q, true)) n++;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// НАГРАДА ЗА ВХОД (7 дней)
|
||||
// =====================================================================
|
||||
|
||||
public bool CanClaimLogin { get { return Data.lastLoginRewardDay != Today; } }
|
||||
|
||||
public void ClaimLogin()
|
||||
{
|
||||
if (!CanClaimLogin) return;
|
||||
int idx = Data.loginRewardIndex % GameData.LoginRewards.Length;
|
||||
Data.lastLoginRewardDay = Today;
|
||||
Data.loginRewardIndex = (idx + 1) % GameData.LoginRewards.Length;
|
||||
Grant(GameData.LoginRewards[idx], 15);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ПОЧТА
|
||||
// =====================================================================
|
||||
|
||||
public void SendMail(string title, string body, params RewardItem[] rewards)
|
||||
{
|
||||
var m = new MailItem { id = Data.nextMailId++, title = title, body = body, sentTicks = DateTime.Now.Ticks };
|
||||
if (rewards != null) m.rewards.AddRange(rewards);
|
||||
Data.mail.Insert(0, m);
|
||||
if (Data.mail.Count > 50) Data.mail.RemoveAt(Data.mail.Count - 1);
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
void SendStarterMail()
|
||||
{
|
||||
Data.starterMailSent = true;
|
||||
SendMail("Как играть",
|
||||
"1. Собирайте ресурсы, нажимая на здания.\n2. Стройте новые здания и улучшайте Ратушу.\n" +
|
||||
"3. Обучайте войска и проходите этапы во вкладке «Бой».\n4. Выполняйте задания и забирайте ежедневные награды.\n" +
|
||||
"5. Сражайтесь на арене и поднимайтесь в рейтинге.");
|
||||
SendMail("Добро пожаловать, командир!",
|
||||
"Королевство ждёт своего правителя. Примите стартовый подарок, чтобы начать строительство.",
|
||||
new RewardItem(ResourceType.Gold, 500), new RewardItem(ResourceType.Wood, 500),
|
||||
new RewardItem(ResourceType.Food, 500), new RewardItem(ResourceType.Gems, 100));
|
||||
}
|
||||
|
||||
public int UnreadMailCount
|
||||
{
|
||||
get { int n = 0; foreach (var m in Data.mail) if (!m.read || (m.HasRewards && !m.claimed)) n++; return n; }
|
||||
}
|
||||
|
||||
public MailItem GetMail(int id) { return Data.mail.Find(m => m.id == id); }
|
||||
|
||||
public void ReadMail(MailItem m) { if (m != null && !m.read) { m.read = true; MarkChanged(); } }
|
||||
|
||||
public void ClaimMail(MailItem m, bool toast = true)
|
||||
{
|
||||
if (m == null || m.claimed || !m.HasRewards) return;
|
||||
m.claimed = true; m.read = true;
|
||||
Grant(m.rewards.ToArray(), 0, toast);
|
||||
}
|
||||
|
||||
public void DeleteMail(MailItem m)
|
||||
{
|
||||
if (m == null) return;
|
||||
if (m.HasRewards && !m.claimed) { UIKit.Toast("Сначала заберите награду из письма"); return; }
|
||||
Data.mail.Remove(m);
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public void ClaimAllMail()
|
||||
{
|
||||
var all = new List<RewardItem>();
|
||||
foreach (var m in Data.mail)
|
||||
{
|
||||
m.read = true;
|
||||
if (m.HasRewards && !m.claimed) { m.claimed = true; all.AddRange(m.rewards); }
|
||||
}
|
||||
if (all.Count > 0) Grant(all.ToArray()); else UIKit.Toast("Все письма прочитаны");
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public void DeleteReadMail()
|
||||
{
|
||||
int removed = Data.mail.RemoveAll(m => m.read && (!m.HasRewards || m.claimed));
|
||||
UIKit.Toast(removed > 0 ? "Удалено писем: " + removed : "Нечего удалять");
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// МАГАЗИН
|
||||
// =====================================================================
|
||||
|
||||
public int BoughtToday(string id)
|
||||
{
|
||||
int i = Data.shopIds.IndexOf(id);
|
||||
return i >= 0 ? Data.shopCounts[i] : 0;
|
||||
}
|
||||
|
||||
public bool CanBuy(ShopItemDef item)
|
||||
{
|
||||
if (item.comingSoon) return false;
|
||||
if (item.dailyLimit > 0 && BoughtToday(item.id) >= item.dailyLimit) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Buy(ShopItemDef item)
|
||||
{
|
||||
if (item.comingSoon) { UIKit.Toast("Покупки за реальные деньги появятся позже"); return; }
|
||||
if (!CanBuy(item)) { UIKit.Toast("Лимит на сегодня исчерпан"); return; }
|
||||
if (item.price > 0 && !Pay(new[] { new RewardItem(item.priceType, item.price) })) return;
|
||||
|
||||
int i = Data.shopIds.IndexOf(item.id);
|
||||
if (i < 0) { Data.shopIds.Add(item.id); Data.shopCounts.Add(1); } else Data.shopCounts[i]++;
|
||||
|
||||
RewardItem[] gives = item.gives;
|
||||
if (item.id == "free_chest")
|
||||
gives = new[] { GameData.FreeChestPool[UnityEngine.Random.Range(0, GameData.FreeChestPool.Length)] };
|
||||
Grant(gives);
|
||||
}
|
||||
|
||||
public bool FreeChestAvailable { get { return BoughtToday("free_chest") == 0; } }
|
||||
|
||||
// =====================================================================
|
||||
// ГЕРОЙ
|
||||
// =====================================================================
|
||||
|
||||
public int HeroMaxLevel { get { return GameData.HeroMaxLevel(TownHallLevel); } }
|
||||
public bool HasHeroSkill(int index) { return Data.heroLevel >= GameData.HeroSkillLevels[index]; }
|
||||
public bool CanUpgradeHero { get { return Data.heroLevel < HeroMaxLevel && CanAfford(GameData.HeroUpgradeCost(Data.heroLevel)); } }
|
||||
|
||||
public void UpgradeHero()
|
||||
{
|
||||
if (Data.heroLevel >= HeroMaxLevel)
|
||||
{
|
||||
UIKit.Toast("Улучшите Ратушу, чтобы поднять предел уровня героя");
|
||||
return;
|
||||
}
|
||||
if (!Pay(GameData.HeroUpgradeCost(Data.heroLevel))) return;
|
||||
Data.heroLevel++;
|
||||
AddXp(10);
|
||||
UIKit.Toast("Герой достиг " + Data.heroLevel + " уровня!");
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ВОЙСКА
|
||||
// =====================================================================
|
||||
|
||||
public int TotalTroops
|
||||
{
|
||||
get { int n = 0; foreach (var t in Data.troops) n += t; return n; }
|
||||
}
|
||||
|
||||
public bool TroopUnlocked(int index) { return TownHallLevel >= GameData.Troops[index].requiredTownHall; }
|
||||
|
||||
public void Train(int index, int count)
|
||||
{
|
||||
var def = GameData.Troops[index];
|
||||
if (!TroopUnlocked(index)) { UIKit.Toast("Нужна Ратуша " + def.requiredTownHall + " уровня"); return; }
|
||||
if (Barracks.Count == 0) { UIKit.Toast("Сначала постройте Казармы"); return; }
|
||||
if (Data.training.Count >= Barracks.QueueSize) { UIKit.Toast("Очередь обучения заполнена. Больше казарм -- длиннее очередь"); return; }
|
||||
if (!Pay(def.cost, count)) return;
|
||||
|
||||
long start = Data.training.Count > 0 ? Data.training[Data.training.Count - 1].endTicks : Now;
|
||||
if (start < Now) start = Now;
|
||||
long end = start + TimeSpan.FromSeconds(def.secondsPerUnit * count).Ticks;
|
||||
Data.training.Add(new TrainingOrder { troopIndex = index, count = count, startTicks = start, endTicks = end });
|
||||
UIKit.Toast("Обучение начато: " + count + " × " + def.name);
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public double TrainingSecondsLeft(TrainingOrder o) { return TimeSpan.FromTicks(Math.Max(0, o.endTicks - Now)).TotalSeconds; }
|
||||
|
||||
public float TrainingProgress(TrainingOrder o)
|
||||
{
|
||||
long total = o.endTicks - o.startTicks;
|
||||
if (total <= 0) return 1f;
|
||||
return Mathf.Clamp01((float)(Now - o.startTicks) / total);
|
||||
}
|
||||
|
||||
public int SpeedUpCost
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.training.Count == 0) return 0;
|
||||
double secs = TrainingSecondsLeft(Data.training[Data.training.Count - 1]);
|
||||
return Mathf.Max(1, Mathf.CeilToInt((float)secs / 30f)); // 1 кристалл за 30 сек
|
||||
}
|
||||
}
|
||||
|
||||
public void SpeedUpTraining()
|
||||
{
|
||||
if (Data.training.Count == 0) return;
|
||||
if (!Pay(new[] { new RewardItem(ResourceType.Gems, SpeedUpCost) })) return;
|
||||
foreach (var o in Data.training) o.endTicks = Now;
|
||||
ProcessTraining();
|
||||
}
|
||||
|
||||
void ProcessTraining()
|
||||
{
|
||||
bool any = false;
|
||||
while (Data.training.Count > 0 && Data.training[0].endTicks <= Now)
|
||||
{
|
||||
var o = Data.training[0];
|
||||
Data.training.RemoveAt(0);
|
||||
Data.troops[o.troopIndex] += o.count;
|
||||
Data.troopsTrained += o.count;
|
||||
Data.dailyTroops += o.count;
|
||||
Data.eventTroopRemainder += o.count;
|
||||
AddEventPoints(Data.eventTroopRemainder / 5);
|
||||
Data.eventTroopRemainder %= 5;
|
||||
UIKit.Toast("Обучено: " + o.count + " × " + GameData.Troops[o.troopIndex].name);
|
||||
any = true;
|
||||
}
|
||||
if (any) MarkChanged();
|
||||
}
|
||||
|
||||
// Потери в бою: убираем процент от каждого типа войск
|
||||
int LoseTroops(float fraction)
|
||||
{
|
||||
if (HasHeroSkill(3)) fraction *= 0.8f;
|
||||
int lost = 0;
|
||||
for (int i = 0; i < Data.troops.Length; i++)
|
||||
{
|
||||
int l = Mathf.FloorToInt(Data.troops[i] * fraction);
|
||||
Data.troops[i] -= l;
|
||||
lost += l;
|
||||
}
|
||||
return lost;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// БОЙ (кампания)
|
||||
// =====================================================================
|
||||
|
||||
public static float WinChance(int myPower, int enemyPower)
|
||||
{
|
||||
if (myPower <= 0) return 0f;
|
||||
float ratio = (float)myPower / Mathf.Max(1, enemyPower);
|
||||
if (ratio >= 2f) return 1f;
|
||||
return Mathf.Clamp(0.5f + (ratio - 1f) * 1.25f, 0.03f, 0.97f);
|
||||
}
|
||||
|
||||
public class BattleResult
|
||||
{
|
||||
public bool win;
|
||||
public string enemy;
|
||||
public int stage;
|
||||
public RewardItem[] rewards;
|
||||
public int xp;
|
||||
public int lost;
|
||||
}
|
||||
|
||||
public BattleResult FightStage()
|
||||
{
|
||||
if (TotalTroops == 0) { UIKit.Toast("Нет войск! Обучите их во вкладке «Войска»"); return null; }
|
||||
|
||||
int stage = Data.battleStage;
|
||||
float chance = WinChance(ArmyPower, GameData.StageEnemyPower(stage));
|
||||
var res = new BattleResult { stage = stage, enemy = GameData.StageName(stage) };
|
||||
res.win = UnityEngine.Random.value < chance;
|
||||
|
||||
if (res.win)
|
||||
{
|
||||
float bonus = HasHeroSkill(2) ? 1.1f : 1f;
|
||||
var list = new List<RewardItem>();
|
||||
foreach (var r in GameData.StageRewards(stage)) list.Add(new RewardItem(r.type, Mathf.RoundToInt(r.amount * bonus)));
|
||||
int gems = GameData.StageGems(stage);
|
||||
if (gems > 0) list.Add(new RewardItem(ResourceType.Gems, gems));
|
||||
res.rewards = list.ToArray();
|
||||
res.xp = GameData.StageXp(stage);
|
||||
res.lost = LoseTroops(UnityEngine.Random.Range(0.05f, 0.12f));
|
||||
Data.battleStage++;
|
||||
Data.battlesWon++;
|
||||
Data.dailyBattles++;
|
||||
AddEventPoints(10);
|
||||
Grant(res.rewards, res.xp, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.rewards = new RewardItem[0];
|
||||
res.lost = LoseTroops(UnityEngine.Random.Range(0.2f, 0.3f));
|
||||
Data.battlesLost++;
|
||||
AddXp(5);
|
||||
}
|
||||
MarkChanged();
|
||||
return res;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// АРЕНА
|
||||
// =====================================================================
|
||||
|
||||
public int ArenaTickets
|
||||
{
|
||||
get
|
||||
{
|
||||
RefreshTickets();
|
||||
return Data.arenaTickets;
|
||||
}
|
||||
}
|
||||
|
||||
void RefreshTickets()
|
||||
{
|
||||
if (Data.arenaTickets >= GameData.ArenaMaxTickets) return;
|
||||
long period = TimeSpan.FromMinutes(GameData.ArenaTicketMinutes).Ticks;
|
||||
long elapsed = Now - Data.arenaTicketTicks;
|
||||
if (elapsed < period) return;
|
||||
int gained = (int)(elapsed / period);
|
||||
Data.arenaTickets = Mathf.Min(GameData.ArenaMaxTickets, Data.arenaTickets + gained);
|
||||
Data.arenaTicketTicks += gained * period;
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public double SecondsToNextTicket
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.arenaTickets >= GameData.ArenaMaxTickets) return 0;
|
||||
long period = TimeSpan.FromMinutes(GameData.ArenaTicketMinutes).Ticks;
|
||||
return TimeSpan.FromTicks(Math.Max(0, Data.arenaTicketTicks + period - Now)).TotalSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
public List<ArenaOpponent> ArenaOpponents
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.arenaOpponents.Count == 0) GenerateOpponents();
|
||||
return Data.arenaOpponents;
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateOpponents()
|
||||
{
|
||||
Data.arenaOpponents.Clear();
|
||||
int baseP = Mathf.Max(150, ArmyPower);
|
||||
float[] mult = { 0.75f, 1.0f, 1.3f }; // лёгкий, равный, сильный
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Data.arenaOpponents.Add(new ArenaOpponent
|
||||
{
|
||||
name = GameData.BotNames[UnityEngine.Random.Range(0, GameData.BotNames.Length)],
|
||||
power = Mathf.RoundToInt(baseP * mult[i] * UnityEngine.Random.Range(0.9f, 1.1f)),
|
||||
rating = Mathf.Max(0, Data.arenaRating + (i - 1) * 60 + UnityEngine.Random.Range(-25, 26)),
|
||||
});
|
||||
}
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public class ArenaResult { public bool win; public string opponent; public int ratingChange; public RewardItem[] rewards; }
|
||||
|
||||
public ArenaResult FightArena(int index)
|
||||
{
|
||||
if (TotalTroops == 0) { UIKit.Toast("Нет войск! Обучите их во вкладке «Войска»"); return null; }
|
||||
if (ArenaTickets <= 0) { UIKit.Toast("Нет билетов. Следующий через " + GameData.FormatTime(SecondsToNextTicket)); return null; }
|
||||
var opp = ArenaOpponents[index];
|
||||
|
||||
if (Data.arenaTickets == GameData.ArenaMaxTickets) Data.arenaTicketTicks = Now;
|
||||
Data.arenaTickets--;
|
||||
|
||||
var res = new ArenaResult { opponent = opp.name };
|
||||
res.win = UnityEngine.Random.value < WinChance(ArmyPower, opp.power);
|
||||
Data.arenaFights++;
|
||||
Data.dailyArena++;
|
||||
AddEventPoints(5);
|
||||
|
||||
if (res.win)
|
||||
{
|
||||
res.ratingChange = 15 + Mathf.Clamp((opp.rating - Data.arenaRating) / 10, 0, 15) + UnityEngine.Random.Range(0, 6);
|
||||
res.rewards = new[] { new RewardItem(ResourceType.Gold, 150), new RewardItem(ResourceType.Gems, 5) };
|
||||
Data.arenaWins++;
|
||||
Grant(res.rewards, 15, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.ratingChange = -UnityEngine.Random.Range(8, 15);
|
||||
res.rewards = new RewardItem[0];
|
||||
AddXp(5);
|
||||
}
|
||||
Data.arenaRating = Mathf.Max(0, Data.arenaRating + res.ratingChange);
|
||||
GenerateOpponents();
|
||||
MarkChanged();
|
||||
return res;
|
||||
}
|
||||
|
||||
// Таблица лидеров: стабильные боты (зависят от ID игрока) + сам игрок
|
||||
public List<ArenaOpponent> Leaderboard()
|
||||
{
|
||||
var list = new List<ArenaOpponent>();
|
||||
int seed;
|
||||
int.TryParse(Data.playerId, out seed); // ID из цифр -> одинаковая таблица при каждом запуске
|
||||
var rnd = new System.Random(seed);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
list.Add(new ArenaOpponent
|
||||
{
|
||||
name = GameData.BotNames[rnd.Next(GameData.BotNames.Length)] + rnd.Next(10, 99),
|
||||
rating = 2300 - i * 90 - rnd.Next(0, 60),
|
||||
});
|
||||
}
|
||||
list.Add(new ArenaOpponent { name = Data.playerName + " (вы)", rating = Data.arenaRating, power = -1 });
|
||||
list.Sort((a, b) => b.rating.CompareTo(a.rating));
|
||||
return list;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// СОБЫТИЕ
|
||||
// =====================================================================
|
||||
|
||||
public void AddEventPoints(int points)
|
||||
{
|
||||
if (points <= 0) return;
|
||||
EnsureDailyAndWeekly();
|
||||
Data.eventPoints += points;
|
||||
MarkChanged();
|
||||
}
|
||||
|
||||
public bool EventTierClaimed(int i) { return Data.eventClaimed.Contains(i); }
|
||||
public bool EventTierReady(int i) { return Data.eventPoints >= GameData.EventTiers[i] && !EventTierClaimed(i); }
|
||||
|
||||
public void ClaimEventTier(int i)
|
||||
{
|
||||
if (!EventTierReady(i)) return;
|
||||
Data.eventClaimed.Add(i);
|
||||
Grant(GameData.EventTierRewards[i], 20);
|
||||
}
|
||||
|
||||
public bool AnyEventTierReady
|
||||
{
|
||||
get { for (int i = 0; i < GameData.EventTiers.Length; i++) if (EventTierReady(i)) return true; return false; }
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// НАСТРОЙКИ
|
||||
// =====================================================================
|
||||
|
||||
public void ApplySettings()
|
||||
{
|
||||
AudioListener.volume = Data.soundVolume;
|
||||
int count = QualitySettings.names.Length;
|
||||
if (count > 0)
|
||||
{
|
||||
int level = Data.quality == 0 ? 0 : (Data.quality == 1 ? count / 2 : count - 1);
|
||||
QualitySettings.SetQualityLevel(level, true);
|
||||
}
|
||||
Application.targetFrameRate = Data.fps60 ? 60 : 30;
|
||||
}
|
||||
|
||||
public void Vibrate()
|
||||
{
|
||||
#if UNITY_ANDROID || UNITY_IOS
|
||||
if (Data.vibration) Handheld.Vibrate();
|
||||
#endif
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// КРАСНЫЕ ЗНАЧКИ НА КНОПКАХ HUD
|
||||
// =====================================================================
|
||||
|
||||
public int BadgeCount(string panelId)
|
||||
{
|
||||
switch (panelId)
|
||||
{
|
||||
case "Quests": return ClaimableQuestsCount;
|
||||
case "Rewards": return CanClaimLogin ? 1 : 0;
|
||||
case "Mail": return UnreadMailCount;
|
||||
case "Events": return AnyEventTierReady ? 1 : 0;
|
||||
case "Shop": return FreeChestAvailable ? 1 : 0;
|
||||
case "Hero": return CanUpgradeHero ? 1 : 0;
|
||||
case "Arena": return ArenaTickets >= GameData.ArenaMaxTickets ? 1 : 0;
|
||||
case "World": return Data.marches.Count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b23537588a39f74cabea0bb5b6f4ca5
|
||||
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// Всё, что сохраняется в progress.json (отдельно от save.json с ресурсами и зданиями).
|
||||
// JsonUtility умеет: числа, строки, bool, массивы, List<> из [Serializable]-классов.
|
||||
|
||||
[Serializable]
|
||||
public class MailItem
|
||||
{
|
||||
public int id;
|
||||
public string title;
|
||||
public string body;
|
||||
public long sentTicks;
|
||||
public bool read;
|
||||
public bool claimed;
|
||||
public List<RewardItem> rewards = new List<RewardItem>();
|
||||
public bool HasRewards { get { return rewards != null && rewards.Count > 0; } }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TrainingOrder
|
||||
{
|
||||
public int troopIndex;
|
||||
public int count;
|
||||
public long startTicks;
|
||||
public long endTicks;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ArenaOpponent
|
||||
{
|
||||
public string name;
|
||||
public int power;
|
||||
public int rating;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ProgressData
|
||||
{
|
||||
// --- Профиль ---
|
||||
public string playerName = "Командир";
|
||||
public string playerId = "";
|
||||
public int avatarIndex = 0;
|
||||
public int level = 1;
|
||||
public int xp = 0;
|
||||
public long firstLaunchTicks = 0;
|
||||
public int daysPlayed = 0;
|
||||
public int lastLoginDay = -1;
|
||||
|
||||
// --- Настройки ---
|
||||
public float musicVolume = 0.8f;
|
||||
public float soundVolume = 0.8f;
|
||||
public bool vibration = true;
|
||||
public bool notifications = true;
|
||||
public int quality = 1; // 0 низкое, 1 среднее, 2 высокое
|
||||
public bool fps60 = true;
|
||||
|
||||
// --- Статистика (для профиля и заданий) ---
|
||||
public float[] collected = new float[8]; // индекс = (int)ResourceType
|
||||
public int battlesWon;
|
||||
public int battlesLost;
|
||||
public int troopsTrained;
|
||||
public int arenaFights;
|
||||
public int arenaWins;
|
||||
public int questsDone;
|
||||
|
||||
// --- Задания ---
|
||||
public List<string> claimedQuests = new List<string>();
|
||||
public int dailyDay = -1;
|
||||
public List<string> claimedDaily = new List<string>();
|
||||
public float[] dailyCollected = new float[8];
|
||||
public int dailyBattles;
|
||||
public int dailyTroops;
|
||||
public int dailyArena;
|
||||
public int dailyMarches;
|
||||
|
||||
// --- Награда за вход ---
|
||||
public int loginRewardIndex = 0; // какой день из 7 следующий
|
||||
public int lastLoginRewardDay = -1;
|
||||
|
||||
// --- Почта ---
|
||||
public List<MailItem> mail = new List<MailItem>();
|
||||
public int nextMailId = 1;
|
||||
public bool starterMailSent;
|
||||
|
||||
// --- Магазин ---
|
||||
public int shopDay = -1;
|
||||
public List<string> shopIds = new List<string>();
|
||||
public List<int> shopCounts = new List<int>();
|
||||
|
||||
// --- Герой ---
|
||||
public int heroLevel = 1;
|
||||
|
||||
// --- Войска ---
|
||||
public int[] troops = new int[4];
|
||||
public List<TrainingOrder> training = new List<TrainingOrder>();
|
||||
|
||||
// --- Бой (кампания) ---
|
||||
public int battleStage = 1;
|
||||
|
||||
// --- Арена ---
|
||||
public int arenaRating = 1000;
|
||||
public int arenaTickets = 5;
|
||||
public long arenaTicketTicks = 0;
|
||||
public List<ArenaOpponent> arenaOpponents = new List<ArenaOpponent>();
|
||||
|
||||
// --- Глобальная карта ---
|
||||
public List<SettlementState> settlements = new List<SettlementState>();
|
||||
public List<WorldMarch> marches = new List<WorldMarch>();
|
||||
public List<BattleReport> reports = new List<BattleReport>();
|
||||
public int nextMarchId = 1;
|
||||
public int marchesSent;
|
||||
public int worldWins;
|
||||
public int worldLosses;
|
||||
|
||||
// --- Большая карта мира (сцена WorldMap, код Саши) ---
|
||||
public List<Bogatyri.World.MarchData> sashaMarches = new List<Bogatyri.World.MarchData>();
|
||||
public List<ArmyBridge.MarchUnits> sashaMarchUnits = new List<ArmyBridge.MarchUnits>();
|
||||
|
||||
// --- Событие ---
|
||||
public int eventWeek = -1;
|
||||
public int eventPoints;
|
||||
public int eventTroopRemainder;
|
||||
public List<int> eventClaimed = new List<int>();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5573100d4811a00429946a55af8f0204
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
// Проверка "палец/мышь сейчас над UI?".
|
||||
// Нужна, чтобы клик по кнопке панели НЕ проходил насквозь в мир:
|
||||
// не собирал ресурсы со здания под панелью, не улучшал Ратушу, не двигал камеру.
|
||||
public static class UIBlocker
|
||||
{
|
||||
public static bool IsPointerOverUI()
|
||||
{
|
||||
if (EventSystem.current == null) return false;
|
||||
|
||||
// мышь (редактор/ПК) и новый Input System
|
||||
if (EventSystem.current.IsPointerOverGameObject()) return true;
|
||||
|
||||
// тач на телефоне (старый input module требует id пальца)
|
||||
for (int i = 0; i < Input.touchCount; i++)
|
||||
{
|
||||
if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(i).fingerId))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54a1703640ad8894cb8f75fb798c6685
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83ba3a90e5d8dbe44a8658c267eab743
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// АРЕНА: бои с другими "игроками" (пока боты) за рейтинг. Билеты: 5 штук, 1 восстанавливается за 20 минут.
|
||||
// Вкладка "Рейтинг" -- таблица лидеров и ваше место. Лиги: Бронза -> Серебро -> Золото -> Платина -> Алмаз.
|
||||
public class ArenaPanel : BasePanel
|
||||
{
|
||||
int tab;
|
||||
TextMeshProUGUI ticketsText;
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var col = UIKit.Card(root, new Color(0, 0, 0, 0), 0, 12);
|
||||
UIKit.Stretch(col);
|
||||
UIKit.Tabs(col, new[] { "Бой", "Рейтинг" }, tab, i => { tab = i; RequestRebuild(); });
|
||||
var holder = UIKit.Make("ListHolder", col);
|
||||
UIKit.Size(holder, -1, -1).flexibleHeight = 1;
|
||||
var list = ScrollList(holder);
|
||||
ticketsText = null;
|
||||
|
||||
if (tab == 0) BuildFights(list); else BuildRating(list);
|
||||
}
|
||||
|
||||
void BuildFights(RectTransform list)
|
||||
{
|
||||
var d = GP.Data;
|
||||
int mine = GP.ArmyPower;
|
||||
|
||||
var head = UIKit.Card(list, new Color(0.15f, 0.2f, 0.35f));
|
||||
UIKit.Text(head, "Рейтинг: " + d.arenaRating + " • Лига: " + GameData.ArenaLeague(d.arenaRating),
|
||||
32, UIKit.Theme.Gold, TextAlignmentOptions.Center).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(head, "Ваша сила: " + mine.ToString("N0") + " • Побед: " + d.arenaWins + " из " + d.arenaFights,
|
||||
24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
ticketsText = UIKit.Text(head, "", 28, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||||
Tick();
|
||||
|
||||
if (GP.TotalTroops == 0)
|
||||
{
|
||||
UIKit.Text(list, "Для боёв на арене нужны войска.", 28, UIKit.Theme.Red, TextAlignmentOptions.Center, 60);
|
||||
UIKit.Button(list, "Перейти к войскам", UIKit.Theme.Blue, () => OpenPanel("Troops"), -1, 90);
|
||||
}
|
||||
|
||||
var opps = GP.ArenaOpponents;
|
||||
string[] tags = { "Лёгкий", "Равный", "Сильный" };
|
||||
for (int i = 0; i < opps.Count; i++)
|
||||
{
|
||||
int idx = i;
|
||||
var o = opps[i];
|
||||
float chance = GameProgress.WinChance(mine, o.power);
|
||||
var card = UIKit.Card(list);
|
||||
var row = UIKit.Row(card, 120, 16);
|
||||
UIKit.Icon(row, ProfilePanel.AvatarColors[(o.name.Length + i) % ProfilePanel.AvatarColors.Length], o.name.Substring(0, 1), 110);
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 2);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
UIKit.Text(info, o.name + " <size=22><color=#c8c8c8>" + (i < tags.Length ? tags[i] : "") + "</color></size>", 30).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, "Сила " + o.power.ToString("N0") + " • рейтинг " + o.rating, 24, UIKit.Theme.TextDim);
|
||||
Color cc = chance >= 0.7f ? UIKit.Theme.Green : (chance >= 0.4f ? UIKit.Theme.Gold : UIKit.Theme.Red);
|
||||
UIKit.Text(info, "Шанс победы " + Mathf.RoundToInt(chance * 100) + "%", 24, cc);
|
||||
|
||||
bool can = GP.TotalTroops > 0 && GP.ArenaTickets > 0;
|
||||
var b = UIKit.Button(row, "Атаковать", can ? UIKit.Theme.Red : UIKit.Theme.Gray, () => Fight(idx), 200, 90);
|
||||
b.interactable = can;
|
||||
}
|
||||
|
||||
UIKit.Button(list, "Обновить соперников", UIKit.Theme.Gray, () => GP.GenerateOpponents(), -1, 80);
|
||||
UIKit.Text(list, "Победа: +15..35 рейтинга, 150 золота, 5 кристаллов. Поражение: -8..14 рейтинга.",
|
||||
22, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
}
|
||||
|
||||
void BuildRating(RectTransform list)
|
||||
{
|
||||
var board = GP.Leaderboard();
|
||||
int myRank = board.FindIndex(e => e.power == -1) + 1;
|
||||
UIKit.Text(list, "Ваше место: " + myRank + " из " + board.Count, 32, UIKit.Theme.Gold, TextAlignmentOptions.Center, 60)
|
||||
.fontStyle = FontStyles.Bold;
|
||||
|
||||
for (int i = 0; i < board.Count; i++)
|
||||
{
|
||||
var e = board[i];
|
||||
bool me = e.power == -1;
|
||||
var row = UIKit.Row(list, 70, 14, me ? new Color(0.45f, 0.36f, 0.12f) : (i % 2 == 0 ? UIKit.Theme.Card : UIKit.Theme.CardDark), 10);
|
||||
Color place = i == 0 ? UIKit.Theme.Gold : (i == 1 ? new Color(0.8f, 0.8f, 0.85f) : (i == 2 ? new Color(0.8f, 0.5f, 0.3f) : UIKit.Theme.TextDim));
|
||||
UIKit.Size(UIKit.Text(row, (i + 1).ToString(), 30, place, TextAlignmentOptions.Center), 70);
|
||||
var n = UIKit.Text(row, e.name, 28, me ? UIKit.Theme.Gold : UIKit.Theme.Text);
|
||||
UIKit.Size(n, -1, -1, 1);
|
||||
UIKit.Size(UIKit.Text(row, GameData.ArenaLeague(e.rating), 24, UIKit.Theme.TextDim, TextAlignmentOptions.Right), 140);
|
||||
UIKit.Size(UIKit.Text(row, e.rating.ToString(), 28, UIKit.Theme.Text, TextAlignmentOptions.Right), 100);
|
||||
}
|
||||
}
|
||||
|
||||
void Fight(int index)
|
||||
{
|
||||
var r = GP.FightArena(index);
|
||||
if (r == null) return;
|
||||
if (r.win)
|
||||
UIKit.Message("ПОБЕДА!", "Вы победили игрока " + r.opponent + "!\n\nРейтинг: +" + r.ratingChange +
|
||||
"\nНаграда: " + GameData.RewardsText(r.rewards) + ", 15 опыта", "Отлично");
|
||||
else
|
||||
UIKit.Message("Поражение", r.opponent + " оказался сильнее.\n\nРейтинг: " + r.ratingChange, "Понятно");
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
if (ticketsText == null) return;
|
||||
int t = GP.ArenaTickets;
|
||||
string next = t < GameData.ArenaMaxTickets ? " • +1 через " + GameData.FormatTime(GP.SecondsToNextTicket) : "";
|
||||
ticketsText.text = "Билеты: " + t + " / " + GameData.ArenaMaxTickets + next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78b7d6900d49bfd449f67923e9f74315
|
||||
@@ -0,0 +1,87 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// БОЙ: кампания из этапов. Сила армии против силы врага -> шанс победы.
|
||||
// Победа: ресурсы, опыт, следующий этап, небольшие потери. Поражение: большие потери войск.
|
||||
public class BattlePanel : BasePanel
|
||||
{
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
int stage = d.battleStage;
|
||||
int enemy = GameData.StageEnemyPower(stage);
|
||||
int mine = GP.ArmyPower;
|
||||
float chance = GameProgress.WinChance(mine, enemy);
|
||||
var list = ScrollList(root);
|
||||
|
||||
// --- Моя армия ---
|
||||
var army = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
var ar = UIKit.Row(army, 60, 10);
|
||||
var at = UIKit.Text(ar, "Ваша армия: " + mine.ToString("N0"), 32, UIKit.Theme.Text);
|
||||
at.fontStyle = FontStyles.Bold;
|
||||
UIKit.Size(at, -1, -1, 1);
|
||||
UIKit.Size(UIKit.Button(ar, "Войска", UIKit.Theme.Gray, () => OpenPanel("Troops"), 160, 60, 24), 160, 60);
|
||||
UIKit.Text(army, "Воинов: " + GP.TotalTroops.ToString("N0") + " • Герой " + d.heroLevel + " ур. (+" +
|
||||
Mathf.RoundToInt(GP.ArmyBonus * 100) + "%)", 24, UIKit.Theme.TextDim);
|
||||
|
||||
// --- Глобальная карта ---
|
||||
var world = UIKit.Card(list, new Color(0.16f, 0.27f, 0.17f));
|
||||
UIKit.Text(world, "Карта мира: отправляйте армию грабить поселения и приносите добычу домой.", 24, UIKit.Theme.Text);
|
||||
UIKit.Button(world, "ОТКРЫТЬ КАРТУ МИРА", UIKit.Theme.Green, () => OpenPanel("World"), -1, 90, 30);
|
||||
|
||||
// --- Текущий этап ---
|
||||
var card = UIKit.Card(list, new Color(0.3f, 0.14f, 0.13f), 24, 12);
|
||||
UIKit.Text(card, "ЭТАП " + stage, 28, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
UIKit.Text(card, GameData.StageName(stage), 42, UIKit.Theme.Gold, TextAlignmentOptions.Center).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(card, "Сила врага: " + enemy.ToString("N0"), 32, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||||
|
||||
Color cc = chance >= 0.7f ? UIKit.Theme.Green : (chance >= 0.4f ? UIKit.Theme.Gold : UIKit.Theme.Red);
|
||||
UIKit.Bar(card, chance, cc, 40, "Шанс победы: " + Mathf.RoundToInt(chance * 100) + "%");
|
||||
|
||||
string rewards = GameData.RewardsText(GameData.StageRewards(stage)) + ", " + GameData.StageXp(stage) + " опыта";
|
||||
if (GameData.StageGems(stage) > 0) rewards += ", " + GameData.StageGems(stage) + " кристаллов";
|
||||
UIKit.Text(card, "Награда: " + rewards, 26, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
|
||||
if (GP.TotalTroops == 0)
|
||||
{
|
||||
UIKit.Text(card, "У вас нет войск! Сначала обучите их.", 28, UIKit.Theme.Red, TextAlignmentOptions.Center);
|
||||
UIKit.Button(card, "Перейти к войскам", UIKit.Theme.Blue, () => OpenPanel("Troops"), -1, 100, 32);
|
||||
}
|
||||
else
|
||||
{
|
||||
UIKit.Button(card, "В БОЙ!", UIKit.Theme.Red, Fight, -1, 120, 44);
|
||||
if (chance < 0.4f)
|
||||
UIKit.Text(card, "Совет: обучите больше войск или улучшите героя.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
}
|
||||
|
||||
// --- Следующие этапы ---
|
||||
var next = UIKit.Card(list);
|
||||
ProfilePanel.Header(next, "Впереди");
|
||||
for (int s = stage + 1; s <= stage + 4; s++)
|
||||
{
|
||||
string extra = GameData.StageGems(s) > 0 ? " • кристаллы" : "";
|
||||
ProfilePanel.KV(next, s + ". " + GameData.StageName(s), "сила " + GameData.StageEnemyPower(s).ToString("N0") + extra);
|
||||
}
|
||||
UIKit.Text(next, "Пройдено этапов: " + (stage - 1) + " • Побед: " + d.battlesWon + " • Поражений: " + d.battlesLost,
|
||||
24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
}
|
||||
|
||||
void Fight()
|
||||
{
|
||||
var r = GP.FightStage();
|
||||
if (r == null) return;
|
||||
if (r.win)
|
||||
{
|
||||
UIKit.Message("ПОБЕДА!",
|
||||
r.enemy + " повержен!\n\nНаграда: " + GameData.RewardsText(r.rewards) + ", " + r.xp + " опыта\n" +
|
||||
"Потери: " + r.lost + " воинов", "Отлично");
|
||||
GP.Vibrate();
|
||||
}
|
||||
else
|
||||
{
|
||||
UIKit.Message("Поражение",
|
||||
"Армия отступила от «" + r.enemy + "».\n\nПотери: " + r.lost + " воинов\n\n" +
|
||||
"Обучите больше войск или улучшите героя и попробуйте снова.", "Понятно");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 619f3860f6abf3343882e23cfae60c1b
|
||||
@@ -0,0 +1,59 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// СОБЫТИЯ: еженедельное событие "Сбор войск". Очки за бои, арену, задания, обучение войск.
|
||||
// Пороги очков открывают награды. В понедельник всё обнуляется -- новое событие.
|
||||
public class EventsPanel : BasePanel
|
||||
{
|
||||
TextMeshProUGUI timer;
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
var list = ScrollList(root);
|
||||
|
||||
var head = UIKit.Card(list, new Color(0.35f, 0.18f, 0.12f));
|
||||
var title = UIKit.Text(head, GameData.EventTitle, 44, UIKit.Theme.Gold, TextAlignmentOptions.Center);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
timer = UIKit.Text(head, "", 26, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||||
Tick();
|
||||
UIKit.Text(head, GameData.EventDescription, 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
|
||||
int maxPoints = GameData.EventTiers[GameData.EventTiers.Length - 1];
|
||||
var prog = UIKit.Card(list);
|
||||
UIKit.Text(prog, "Ваши очки: " + d.eventPoints, 34, UIKit.Theme.Text, TextAlignmentOptions.Center).fontStyle = FontStyles.Bold;
|
||||
UIKit.Bar(prog, (float)d.eventPoints / maxPoints, UIKit.Theme.Gold, 34, d.eventPoints + " / " + maxPoints);
|
||||
|
||||
for (int i = 0; i < GameData.EventTiers.Length; i++)
|
||||
{
|
||||
int idx = i;
|
||||
bool claimed = GP.EventTierClaimed(i);
|
||||
bool ready = GP.EventTierReady(i);
|
||||
|
||||
var card = UIKit.Card(list, ready ? UIKit.Theme.CardDone : (claimed ? UIKit.Theme.CardDark : UIKit.Theme.Card));
|
||||
var row = UIKit.Row(card, 100, 14);
|
||||
UIKit.Icon(row, ready ? UIKit.Theme.Green : UIKit.Theme.Gray, (i + 1).ToString(), 90);
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 4);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
UIKit.Text(info, GameData.EventTiers[i] + " очков", 30).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, GameData.RewardsText(GameData.EventTierRewards[i]) + ", 20 опыта", 24, UIKit.Theme.TextDim);
|
||||
|
||||
string label = claimed ? "Получено" : (ready ? "Забрать" : "Нужно ещё " + (GameData.EventTiers[i] - d.eventPoints));
|
||||
var b = UIKit.Button(row, label, ready ? UIKit.Theme.Green : UIKit.Theme.Gray, () => GP.ClaimEventTier(idx), 230, 90, 24);
|
||||
b.interactable = ready;
|
||||
}
|
||||
|
||||
// Быстрые переходы к источникам очков
|
||||
var go = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
UIKit.Text(go, "Где заработать очки:", 28, UIKit.Theme.TextDim);
|
||||
var r = UIKit.Row(go, 80, 10);
|
||||
UIKit.Size(UIKit.Button(r, "Бой", UIKit.Theme.Red, () => OpenPanel("Battle")), -1, 80, 1);
|
||||
UIKit.Size(UIKit.Button(r, "Арена", UIKit.Theme.Blue, () => OpenPanel("Arena")), -1, 80, 1);
|
||||
UIKit.Size(UIKit.Button(r, "Войска", UIKit.Theme.Gray, () => OpenPanel("Troops")), -1, 80, 1);
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
if (timer != null) timer.text = "До конца события: " + GameData.FormatTime(GameProgress.SecondsToWeekEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35abdf9dc29995549b2c60dc6e2da580
|
||||
@@ -0,0 +1,78 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// ГЕРОЙ: уровень, характеристики, бонус к армии, прокачка за золото и еду, навыки.
|
||||
// Предел уровня героя = Ратуша * 5 -- чтобы качать героя дальше, нужно улучшать Ратушу.
|
||||
public class HeroPanel : BasePanel
|
||||
{
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
int lvl = d.heroLevel;
|
||||
int maxLvl = GP.HeroMaxLevel;
|
||||
var list = ScrollList(root);
|
||||
|
||||
// --- Портрет и уровень ---
|
||||
var head = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
var row = UIKit.Row(head, 180, 20);
|
||||
UIKit.Icon(row, new Color(0.55f, 0.4f, 0.2f), "Г", 180);
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 6);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
UIKit.Text(info, "Верховный командир", 36, UIKit.Theme.Gold).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, "Уровень " + lvl + " / " + maxLvl, 30);
|
||||
UIKit.Text(info, "Сила героя: " + GameData.HeroPower(lvl), 26, UIKit.Theme.TextDim);
|
||||
UIKit.Bar(head, (float)lvl / GameData.HeroMaxLevel(10), UIKit.Theme.Gold, 24, "Прогресс к максимуму: " + lvl + " / " + GameData.HeroMaxLevel(10));
|
||||
|
||||
// --- Характеристики ---
|
||||
var stats = UIKit.Card(list);
|
||||
ProfilePanel.Header(stats, "Характеристики");
|
||||
StatRow(stats, "Атака", GameData.HeroAttack(lvl), GameData.HeroAttack(lvl + 1));
|
||||
StatRow(stats, "Защита", GameData.HeroDefense(lvl), GameData.HeroDefense(lvl + 1));
|
||||
StatRow(stats, "Бонус к силе армии, %", GameData.HeroArmyBonusPercent(lvl), GameData.HeroArmyBonusPercent(lvl + 1));
|
||||
StatRow(stats, "Сила героя", GameData.HeroPower(lvl), GameData.HeroPower(lvl + 1));
|
||||
ProfilePanel.KV(stats, "Итоговый бонус армии", "+" + Mathf.RoundToInt(GP.ArmyBonus * 100) + "%");
|
||||
|
||||
// --- Улучшение ---
|
||||
var up = UIKit.Card(list, UIKit.Theme.Card);
|
||||
ProfilePanel.Header(up, "Улучшение");
|
||||
if (lvl >= maxLvl)
|
||||
{
|
||||
UIKit.Text(up, "Достигнут предел уровня. Улучшите Ратушу до " + (GP.TownHallLevel + 1) +
|
||||
" уровня, чтобы поднять предел до " + GameData.HeroMaxLevel(GP.TownHallLevel + 1) + ".", 28, UIKit.Theme.TextDim);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cost = GameData.HeroUpgradeCost(lvl);
|
||||
foreach (var c in cost)
|
||||
{
|
||||
bool enough = GP.Res(c.type) >= c.amount;
|
||||
ProfilePanel.KV(up, GameData.ResName(c.type),
|
||||
"<color=" + (enough ? "#9be59b" : "#ff7a7a") + ">" + c.amount.ToString("N0") + "</color> / " +
|
||||
Mathf.FloorToInt(GP.Res(c.type)).ToString("N0"));
|
||||
}
|
||||
bool can = GP.CanUpgradeHero;
|
||||
var b = UIKit.Button(up, can ? "УЛУЧШИТЬ ДО " + (lvl + 1) + " УР." : "Не хватает ресурсов",
|
||||
can ? UIKit.Theme.Green : UIKit.Theme.Gray, () => GP.UpgradeHero(), -1, 100, 32);
|
||||
b.interactable = can;
|
||||
}
|
||||
|
||||
// --- Навыки ---
|
||||
var skills = UIKit.Card(list);
|
||||
ProfilePanel.Header(skills, "Навыки");
|
||||
for (int i = 0; i < GameData.HeroSkillNames.Length; i++)
|
||||
{
|
||||
bool has = GP.HasHeroSkill(i);
|
||||
var r = UIKit.Row(skills, 70, 14);
|
||||
UIKit.Icon(r, has ? UIKit.Theme.Gold : UIKit.Theme.Gray, (i + 1).ToString(), 56);
|
||||
var t = UIKit.Text(r, GameData.HeroSkillNames[i], 26, has ? UIKit.Theme.Text : UIKit.Theme.TextDim);
|
||||
UIKit.Size(t, -1, -1, 1);
|
||||
UIKit.Size(UIKit.Text(r, has ? "Открыт" : "с " + GameData.HeroSkillLevels[i] + " ур.", 24,
|
||||
has ? UIKit.Theme.Green : UIKit.Theme.TextDim, TextAlignmentOptions.Right), 140);
|
||||
}
|
||||
}
|
||||
|
||||
static void StatRow(Transform parent, string name, int now, int next)
|
||||
{
|
||||
ProfilePanel.KV(parent, name, now + " <color=#9be59b>(+" + (next - now) + ")</color>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f65b505a260db5b4c9842e18522033e5
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// ПОЧТА: список писем (новые подсвечены), чтение письма, подарки во вложениях,
|
||||
// "Забрать всё" и "Удалить прочитанные". Письма приходят сами: приветствие, новый уровень и т.д.
|
||||
public class MailPanel : BasePanel
|
||||
{
|
||||
int openedId; // 0 -- показываем список
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
openedId = 0;
|
||||
}
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var mail = openedId != 0 ? GP.GetMail(openedId) : null;
|
||||
if (mail == null) { openedId = 0; BuildList(root); }
|
||||
else BuildLetter(root, mail);
|
||||
}
|
||||
|
||||
void BuildList(RectTransform root)
|
||||
{
|
||||
var col = UIKit.Card(root, new Color(0, 0, 0, 0), 0, 12);
|
||||
UIKit.Stretch(col);
|
||||
|
||||
var top = UIKit.Row(col, 80, 12);
|
||||
UIKit.Size(UIKit.Button(top, "Забрать всё", UIKit.Theme.Green, () => GP.ClaimAllMail()), -1, 80, 1);
|
||||
UIKit.Size(UIKit.Button(top, "Удалить прочитанные", UIKit.Theme.Gray, () => GP.DeleteReadMail()), -1, 80, 1);
|
||||
|
||||
var holder = UIKit.Make("ListHolder", col);
|
||||
UIKit.Size(holder, -1, -1).flexibleHeight = 1;
|
||||
var list = ScrollList(holder, 10);
|
||||
|
||||
if (GP.Data.mail.Count == 0)
|
||||
{
|
||||
UIKit.Text(list, "Писем нет", 32, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 150);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var m in GP.Data.mail)
|
||||
{
|
||||
var mm = m;
|
||||
bool unread = !m.read;
|
||||
bool gift = m.HasRewards && !m.claimed;
|
||||
var card = UIKit.Card(list, unread ? new Color(0.24f, 0.24f, 0.3f) : UIKit.Theme.CardDark, 16, 4);
|
||||
var btn = card.gameObject.AddComponent<Button>();
|
||||
btn.onClick.AddListener(() => { openedId = mm.id; GP.ReadMail(mm); RequestRebuild(); });
|
||||
|
||||
var row = UIKit.Row(card, 46, 10);
|
||||
var title = UIKit.Text(row, m.title, 30, unread ? UIKit.Theme.Gold : UIKit.Theme.Text);
|
||||
title.fontStyle = unread ? FontStyles.Bold : FontStyles.Normal;
|
||||
UIKit.Size(title, -1, -1, 1);
|
||||
if (gift) UIKit.Size(UIKit.Text(row, "ПОДАРОК", 22, UIKit.Theme.Green, TextAlignmentOptions.Right), 150);
|
||||
|
||||
UIKit.Text(card, new DateTime(m.sentTicks).ToString("dd.MM.yyyy HH:mm") + (unread ? " • новое" : ""),
|
||||
22, UIKit.Theme.TextDim);
|
||||
}
|
||||
}
|
||||
|
||||
void BuildLetter(RectTransform root, MailItem m)
|
||||
{
|
||||
var list = ScrollList(root);
|
||||
UIKit.Button(list, "< Назад к письмам", UIKit.Theme.Gray, () => { openedId = 0; RequestRebuild(); }, -1, 76);
|
||||
|
||||
var card = UIKit.Card(list, UIKit.Theme.Card, 24, 14);
|
||||
var title = UIKit.Text(card, m.title, 36, UIKit.Theme.Gold);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(card, new DateTime(m.sentTicks).ToString("dd.MM.yyyy HH:mm"), 22, UIKit.Theme.TextDim);
|
||||
UIKit.Text(card, m.body, 30);
|
||||
|
||||
if (m.HasRewards)
|
||||
{
|
||||
var gift = UIKit.Card(list, m.claimed ? UIKit.Theme.CardDark : UIKit.Theme.CardDone);
|
||||
ProfilePanel.Header(gift, "Вложения");
|
||||
foreach (var r in m.rewards)
|
||||
{
|
||||
var row = UIKit.Row(gift, 50, 12);
|
||||
UIKit.Icon(row, GameData.ResColor(r.type), "", 36);
|
||||
var n = UIKit.Text(row, GameData.ResName(r.type), 28);
|
||||
UIKit.Size(n, -1, -1, 1);
|
||||
UIKit.Size(UIKit.Text(row, r.amount.ToString("N0"), 28, UIKit.Theme.Gold, TextAlignmentOptions.Right), 200);
|
||||
}
|
||||
var b = UIKit.Button(gift, m.claimed ? "Получено" : "Забрать", m.claimed ? UIKit.Theme.Gray : UIKit.Theme.Green,
|
||||
() => GP.ClaimMail(m), -1, 90);
|
||||
b.interactable = !m.claimed;
|
||||
}
|
||||
|
||||
UIKit.Button(list, "Удалить письмо", UIKit.Theme.Red, () =>
|
||||
{
|
||||
if (m.HasRewards && !m.claimed) { UIKit.Toast("Сначала заберите подарок"); return; }
|
||||
GP.DeleteMail(m); openedId = 0; RequestRebuild();
|
||||
}, -1, 80);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bff136fda8c360499ec2c3915d622ab
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// ПРОФИЛЬ: аватар, имя, ID, уровень и опыт, сила, королевство, все ресурсы, статистика.
|
||||
public class ProfilePanel : BasePanel
|
||||
{
|
||||
public static readonly Color[] AvatarColors =
|
||||
{
|
||||
new Color(0.3f, 0.45f, 0.8f), new Color(0.75f, 0.3f, 0.3f), new Color(0.3f, 0.65f, 0.4f),
|
||||
new Color(0.7f, 0.5f, 0.2f), new Color(0.55f, 0.35f, 0.7f), new Color(0.25f, 0.6f, 0.65f),
|
||||
};
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
var list = ScrollList(root);
|
||||
|
||||
// --- Шапка: аватар + имя ---
|
||||
var head = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
var row = UIKit.Row(head, 150, 20);
|
||||
string letter = d.playerName.Length > 0 ? d.playerName.Substring(0, 1).ToUpper() : "?";
|
||||
UIKit.Icon(row, AvatarColors[d.avatarIndex % AvatarColors.Length], letter, 150);
|
||||
|
||||
var info = UIKit.Card(row, UIKit.Theme.CardDark, 0, 4);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
var name = UIKit.Text(info, d.playerName, 40, UIKit.Theme.Text);
|
||||
name.fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, "ID: " + d.playerId, 24, UIKit.Theme.TextDim);
|
||||
UIKit.Text(info, "Уровень " + d.level + " • " + GameData.ArenaLeague(d.arenaRating), 28, UIKit.Theme.Gold);
|
||||
|
||||
var btns = UIKit.Row(head, 80, 12);
|
||||
var bName = UIKit.Button(btns, "Сменить имя", UIKit.Theme.Blue, () =>
|
||||
UIKit.InputDialog("Имя командира", d.playerName, 16, v => { d.playerName = v; GP.MarkChanged(); UIKit.Toast("Имя изменено"); }));
|
||||
UIKit.Size(bName, -1, 80, 1);
|
||||
var bAva = UIKit.Button(btns, "Сменить аватар", UIKit.Theme.Gray, () =>
|
||||
{
|
||||
d.avatarIndex = (d.avatarIndex + 1) % AvatarColors.Length;
|
||||
GP.MarkChanged();
|
||||
});
|
||||
UIKit.Size(bAva, -1, 80, 1);
|
||||
|
||||
// --- Опыт ---
|
||||
var xpCard = UIKit.Card(list);
|
||||
bool max = d.level >= GameData.MaxPlayerLevel;
|
||||
int need = GameData.XpToNextLevel(d.level);
|
||||
UIKit.Text(xpCard, max ? "Максимальный уровень" : "Опыт до " + (d.level + 1) + " уровня", 26, UIKit.Theme.TextDim);
|
||||
UIKit.Bar(xpCard, max ? 1f : (float)d.xp / need, UIKit.Theme.Blue, 40, max ? "MAX" : d.xp + " / " + need);
|
||||
|
||||
// --- Сила ---
|
||||
var power = UIKit.Card(list);
|
||||
var pt = UIKit.Text(power, "СИЛА: " + GP.TotalPower.ToString("N0"), 44, GameData.ResColor(ResourceType.Power), TextAlignmentOptions.Center);
|
||||
pt.fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(power, "Армия " + GP.ArmyPower.ToString("N0") + " • Ратуша " + (GP.TownHallLevel * 100) +
|
||||
" • Здания " + (GP.BuildingsCount * 25), 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
|
||||
// --- Королевство ---
|
||||
var kingdom = UIKit.Card(list);
|
||||
Header(kingdom, "Королевство");
|
||||
KV(kingdom, "Ратуша", GP.TownHallLevel + " ур.");
|
||||
KV(kingdom, "Построено зданий", GP.BuildingsCount.ToString());
|
||||
KV(kingdom, "Герой", d.heroLevel + " ур.");
|
||||
KV(kingdom, "Войска", GP.TotalTroops.ToString("N0"));
|
||||
KV(kingdom, "Этап кампании", d.battleStage.ToString());
|
||||
|
||||
// --- Ресурсы ---
|
||||
var res = UIKit.Card(list);
|
||||
Header(res, "Ресурсы");
|
||||
foreach (ResourceType t in Enum.GetValues(typeof(ResourceType)))
|
||||
{
|
||||
if (t == ResourceType.Power) continue;
|
||||
var r = UIKit.Row(res, 56, 14);
|
||||
UIKit.Icon(r, GameData.ResColor(t), "", 40);
|
||||
var n = UIKit.Text(r, GameData.ResName(t), 28);
|
||||
UIKit.Size(n, -1, -1, 1);
|
||||
var v = UIKit.Text(r, Mathf.FloorToInt(GP.Res(t)).ToString("N0"), 28, UIKit.Theme.Gold, TextAlignmentOptions.Right);
|
||||
UIKit.Size(v, 260);
|
||||
}
|
||||
|
||||
// --- Статистика ---
|
||||
var stats = UIKit.Card(list);
|
||||
Header(stats, "Статистика");
|
||||
KV(stats, "Боёв выиграно / проиграно", d.battlesWon + " / " + d.battlesLost);
|
||||
KV(stats, "Карта мира: походов / побед", d.marchesSent + " / " + d.worldWins);
|
||||
KV(stats, "Рейтинг арены", d.arenaRating + " (" + GameData.ArenaLeague(d.arenaRating) + ")");
|
||||
KV(stats, "Побед на арене", d.arenaWins + " из " + d.arenaFights);
|
||||
KV(stats, "Воинов обучено", d.troopsTrained.ToString("N0"));
|
||||
KV(stats, "Заданий выполнено", d.questsDone.ToString());
|
||||
float total = 0; foreach (var c in d.collected) total += c;
|
||||
KV(stats, "Ресурсов собрано", GameData.Short(total));
|
||||
KV(stats, "Дней в игре", d.daysPlayed.ToString());
|
||||
KV(stats, "Играет с", new DateTime(d.firstLaunchTicks, DateTimeKind.Utc).ToLocalTime().ToString("dd.MM.yyyy"));
|
||||
}
|
||||
|
||||
public static void Header(Transform parent, string text)
|
||||
{
|
||||
var t = UIKit.Text(parent, text, 32, UIKit.Theme.Gold);
|
||||
t.fontStyle = FontStyles.Bold;
|
||||
}
|
||||
|
||||
public static void KV(Transform parent, string key, string value)
|
||||
{
|
||||
var r = UIKit.Row(parent, 50, 10);
|
||||
var k = UIKit.Text(r, key, 27, UIKit.Theme.TextDim);
|
||||
UIKit.Size(k, -1, -1, 1);
|
||||
var v = UIKit.Text(r, value, 27, UIKit.Theme.Text, TextAlignmentOptions.Right);
|
||||
UIKit.Size(v, 300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38a461aef5dc6674c8f0b1831b2e50fa
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// ЗАДАНИЯ: две вкладки -- основная цепочка и ежедневные (сброс в полночь).
|
||||
// Прогресс считается сам из игры: сбор ресурсов, стройка, Ратуша, бои, войска, герой, арена.
|
||||
public class QuestsPanel : BasePanel
|
||||
{
|
||||
int tab;
|
||||
TextMeshProUGUI resetTimer;
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var col = UIKit.Card(root, new Color(0, 0, 0, 0), 0, 12);
|
||||
UIKit.Stretch(col);
|
||||
UIKit.Tabs(col, new[] { "Основные", "Ежедневные" }, tab, i => { tab = i; RequestRebuild(); });
|
||||
|
||||
bool daily = tab == 1;
|
||||
resetTimer = null;
|
||||
if (daily)
|
||||
{
|
||||
resetTimer = UIKit.Text(col, "", 26, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 40);
|
||||
Tick();
|
||||
}
|
||||
|
||||
var holder = UIKit.Make("ListHolder", col);
|
||||
UIKit.Size(holder, -1, -1).flexibleHeight = 1;
|
||||
var list = ScrollList(holder);
|
||||
|
||||
var source = daily ? GameData.DailyQuests : GameData.MainQuests;
|
||||
var shown = new List<QuestDef>();
|
||||
int claimedCount = 0;
|
||||
|
||||
// Сначала готовые к получению, потом в процессе. В основных -- не больше 6 активных.
|
||||
foreach (var q in source) if (!GP.IsQuestClaimed(q, daily) && GP.IsQuestDone(q, daily)) shown.Add(q);
|
||||
foreach (var q in source)
|
||||
{
|
||||
if (GP.IsQuestClaimed(q, daily)) { claimedCount++; continue; }
|
||||
if (GP.IsQuestDone(q, daily)) continue;
|
||||
if (!daily && shown.Count >= 6) continue;
|
||||
shown.Add(q);
|
||||
}
|
||||
|
||||
foreach (var q in shown) QuestCard(list, q, daily);
|
||||
|
||||
if (shown.Count == 0)
|
||||
UIKit.Text(list, daily ? "Все ежедневные задания выполнены! Приходите завтра." : "Все задания выполнены! Скоро будут новые.",
|
||||
30, UIKit.Theme.Gold, TextAlignmentOptions.Center, 120);
|
||||
|
||||
UIKit.Text(list, "Выполнено: " + claimedCount + " из " + source.Count, 26, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 50);
|
||||
}
|
||||
|
||||
void QuestCard(Transform parent, QuestDef q, bool daily)
|
||||
{
|
||||
bool done = GP.IsQuestDone(q, daily);
|
||||
int progress = Mathf.Min(GP.QuestProgress(q, daily), q.target);
|
||||
|
||||
var card = UIKit.Card(parent, done ? UIKit.Theme.CardDone : UIKit.Theme.Card);
|
||||
var title = UIKit.Text(card, q.title, 32);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
UIKit.Bar(card, (float)progress / q.target, done ? UIKit.Theme.Green : UIKit.Theme.Gold, 30, progress + " / " + q.target);
|
||||
|
||||
var row = UIKit.Row(card, 80, 12);
|
||||
var reward = UIKit.Text(row, "Награда: " + GameData.RewardsText(q.rewards) + ", " + q.xp + " опыта", 24, UIKit.Theme.TextDim);
|
||||
UIKit.Size(reward, -1, -1, 1);
|
||||
var b = UIKit.Button(row, done ? "Забрать" : "В процессе", done ? UIKit.Theme.Green : UIKit.Theme.Gray,
|
||||
() => GP.ClaimQuest(q, daily), 220, 80);
|
||||
b.interactable = done;
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
if (resetTimer != null) resetTimer.text = "Обновление через " + GameData.FormatTime(GameProgress.SecondsToNextDay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f03a70f374e3ea4c87b82f82e7e270e
|
||||
@@ -0,0 +1,56 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// НАГРАДЫ: ежедневный вход, календарь на 7 дней. Один раз в сутки -- забрать подарок дня.
|
||||
public class RewardsPanel : BasePanel
|
||||
{
|
||||
TextMeshProUGUI timer;
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
var list = ScrollList(root);
|
||||
|
||||
UIKit.Text(list, "Заходите каждый день и забирайте подарки. На 7-й день -- главный приз!",
|
||||
28, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
|
||||
bool canClaim = GP.CanClaimLogin;
|
||||
int total = GameData.LoginRewards.Length;
|
||||
int claimedUpTo = d.loginRewardIndex; // сколько дней цикла уже забрано
|
||||
if (!canClaim && claimedUpTo == 0 && d.lastLoginRewardDay >= 0) claimedUpTo = total; // только что забрали 7-й
|
||||
|
||||
RectTransform row = null;
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (i % 4 == 0) row = UIKit.Row(list, 230, 12);
|
||||
bool claimed = i < claimedUpTo;
|
||||
bool current = canClaim && i == d.loginRewardIndex;
|
||||
|
||||
Color bg = claimed ? UIKit.Theme.CardDone : (current ? new Color(0.45f, 0.36f, 0.12f) : UIKit.Theme.Card);
|
||||
var cell = UIKit.Card(row, bg, 10, 6);
|
||||
UIKit.Size(cell, -1, -1, 1);
|
||||
var day = UIKit.Text(cell, (i == total - 1 ? "ДЕНЬ 7!" : "День " + (i + 1)), 28,
|
||||
current ? UIKit.Theme.Gold : UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||||
day.fontStyle = FontStyles.Bold;
|
||||
var r = GameData.LoginRewards[i];
|
||||
UIKit.Box(cell, GameData.ResColor(r[0].type), 12);
|
||||
UIKit.Text(cell, GameData.RewardsText(r), 22, UIKit.Theme.Text, TextAlignmentOptions.Center);
|
||||
UIKit.Text(cell, claimed ? "Получено" : (current ? "Сегодня" : ""), 22,
|
||||
claimed ? UIKit.Theme.Green : UIKit.Theme.Gold, TextAlignmentOptions.Center);
|
||||
}
|
||||
|
||||
UIKit.Spacer(list, 10);
|
||||
var b = UIKit.Button(list, canClaim ? "ЗАБРАТЬ ПОДАРОК" : "Подарок получен", canClaim ? UIKit.Theme.Green : UIKit.Theme.Gray,
|
||||
() => GP.ClaimLogin(), -1, 110, 36);
|
||||
b.interactable = canClaim;
|
||||
|
||||
timer = UIKit.Text(list, "", 26, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 50);
|
||||
Tick();
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
if (timer == null) return;
|
||||
timer.text = GP.CanClaimLogin ? "" : "Следующий подарок через " + GameData.FormatTime(GameProgress.SecondsToNextDay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4a45b2e2b0cbbf4eadbd85526ecf379
|
||||
@@ -0,0 +1,80 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// НАСТРОЙКИ: звук, вибрация, уведомления, графика, частота кадров, сохранение, сброс, о игре.
|
||||
public class SettingsPanel : BasePanel
|
||||
{
|
||||
// Ползунки не должны пересоздаваться посреди перетаскивания
|
||||
protected override bool RebuildOnChange { get { return false; } }
|
||||
|
||||
static readonly string[] QualityNames = { "Низкое", "Среднее", "Высокое" };
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
var list = ScrollList(root);
|
||||
|
||||
// --- Звук ---
|
||||
var sound = UIKit.Card(list);
|
||||
ProfilePanel.Header(sound, "Звук");
|
||||
SliderRow(sound, "Музыка", d.musicVolume, v => { d.musicVolume = v; GP.ApplySettings(); GP.SaveSoon(); });
|
||||
SliderRow(sound, "Звуки", d.soundVolume, v => { d.soundVolume = v; GP.ApplySettings(); GP.SaveSoon(); });
|
||||
|
||||
// --- Общие ---
|
||||
var general = UIKit.Card(list);
|
||||
ProfilePanel.Header(general, "Общие");
|
||||
SwitchRow(general, "Вибрация", d.vibration, v => { d.vibration = v; GP.SaveSoon(); if (v) GP.Vibrate(); });
|
||||
SwitchRow(general, "Уведомления", d.notifications, v => { d.notifications = v; GP.SaveSoon(); });
|
||||
|
||||
// --- Графика ---
|
||||
var gfx = UIKit.Card(list);
|
||||
ProfilePanel.Header(gfx, "Графика");
|
||||
UIKit.Text(gfx, "Качество", 28, UIKit.Theme.TextDim);
|
||||
UIKit.Tabs(gfx, QualityNames, d.quality, i =>
|
||||
{
|
||||
d.quality = i; GP.ApplySettings(); GP.SaveSoon(); RequestRebuild();
|
||||
});
|
||||
SwitchRow(gfx, "60 кадров/сек (больше расход батареи)", d.fps60, v => { d.fps60 = v; GP.ApplySettings(); GP.SaveSoon(); });
|
||||
|
||||
// --- Игра ---
|
||||
var game = UIKit.Card(list);
|
||||
ProfilePanel.Header(game, "Игра");
|
||||
UIKit.Button(game, "Сохранить игру сейчас", UIKit.Theme.Blue, () =>
|
||||
{
|
||||
if (SaveManager.Instance != null) SaveManager.Instance.SaveGame();
|
||||
GP.Save();
|
||||
UIKit.Toast("Игра сохранена");
|
||||
}, -1, 90);
|
||||
UIKit.Button(game, "Сбросить весь прогресс", UIKit.Theme.Red, () =>
|
||||
UIKit.Confirm("Сбросить прогресс?",
|
||||
"Удалятся все здания, ресурсы, войска, герой и задания. Отменить это нельзя.",
|
||||
"Сбросить", () => { GP.ResetAll(); if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll(); }),
|
||||
-1, 90);
|
||||
|
||||
// --- О игре ---
|
||||
var about = UIKit.Card(list);
|
||||
ProfilePanel.Header(about, "О игре");
|
||||
ProfilePanel.KV(about, "Версия", Application.version);
|
||||
ProfilePanel.KV(about, "ID игрока", d.playerId);
|
||||
UIKit.Text(about, "MVP1 • команда 4bit", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
}
|
||||
|
||||
static void SliderRow(Transform parent, string label, float value, System.Action<float> onChange)
|
||||
{
|
||||
var r = UIKit.Row(parent, 70, 20);
|
||||
var t = UIKit.Text(r, label, 28);
|
||||
UIKit.Size(t, 200);
|
||||
var pct = UIKit.Text(r, Mathf.RoundToInt(value * 100) + "%", 26, UIKit.Theme.TextDim, TextAlignmentOptions.Right);
|
||||
UIKit.Slider(r, value, v => { pct.text = Mathf.RoundToInt(v * 100) + "%"; onChange(v); });
|
||||
pct.transform.SetAsLastSibling();
|
||||
UIKit.Size(pct, 90);
|
||||
}
|
||||
|
||||
static void SwitchRow(Transform parent, string label, bool value, System.Action<bool> onChange)
|
||||
{
|
||||
var r = UIKit.Row(parent, 76, 20);
|
||||
var t = UIKit.Text(r, label, 28);
|
||||
UIKit.Size(t, -1, -1, 1);
|
||||
UIKit.Switch(r, value, onChange);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f269d44d36aae44b8bdee765662fb45
|
||||
@@ -0,0 +1,66 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
// МАГАЗИН: бесплатный сундук раз в день, пакеты ресурсов за кристаллы, обмен золота,
|
||||
// витрина платных наборов (пока "скоро"). Лимиты покупок сбрасываются в полночь.
|
||||
public class ShopPanel : BasePanel
|
||||
{
|
||||
TextMeshProUGUI timer;
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var list = ScrollList(root);
|
||||
|
||||
var wallet = UIKit.Row(list, 70, 20, UIKit.Theme.CardDark, 10);
|
||||
WalletItem(wallet, ResourceType.Gems);
|
||||
WalletItem(wallet, ResourceType.Gold);
|
||||
|
||||
timer = UIKit.Text(list, "", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center, 40);
|
||||
Tick();
|
||||
|
||||
foreach (var item in GameData.ShopItems)
|
||||
{
|
||||
var it = item;
|
||||
bool free = item.price == 0 && !item.comingSoon;
|
||||
int bought = GP.BoughtToday(item.id);
|
||||
bool canBuy = GP.CanBuy(item);
|
||||
|
||||
var card = UIKit.Card(list, free && canBuy ? UIKit.Theme.CardDone : UIKit.Theme.Card);
|
||||
var row = UIKit.Row(card, 130, 16);
|
||||
|
||||
var iconColor = item.gives != null && item.gives.Length > 0 ? GameData.ResColor(item.gives[0].type) : UIKit.Theme.Gold;
|
||||
UIKit.Icon(row, iconColor, item.id == "free_chest" ? "?" : "", 110);
|
||||
|
||||
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 4);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
var title = UIKit.Text(info, item.title, 30);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
string gives = item.id == "free_chest" ? "Случайный набор ресурсов" : GameData.RewardsText(item.gives);
|
||||
UIKit.Text(info, gives, 24, UIKit.Theme.TextDim);
|
||||
if (item.dailyLimit > 0)
|
||||
UIKit.Text(info, "Сегодня: " + bought + " / " + item.dailyLimit, 22, UIKit.Theme.TextDim);
|
||||
|
||||
string label;
|
||||
Color color;
|
||||
if (item.comingSoon) { label = item.priceLabel + "\nскоро"; color = UIKit.Theme.Gray; }
|
||||
else if (!canBuy) { label = "Лимит"; color = UIKit.Theme.Gray; }
|
||||
else if (free) { label = "Бесплатно"; color = UIKit.Theme.Green; }
|
||||
else { label = item.price + " " + GameData.ResName(item.priceType); color = UIKit.Theme.Blue; }
|
||||
|
||||
var b = UIKit.Button(row, label, color, () => GP.Buy(it), 220, 100, 26);
|
||||
b.interactable = canBuy;
|
||||
}
|
||||
}
|
||||
|
||||
static void WalletItem(Transform parent, ResourceType t)
|
||||
{
|
||||
UIKit.Icon(parent, GameData.ResColor(t), "", 40);
|
||||
var txt = UIKit.Text(parent, GameData.ResName(t) + ": " + Mathf.FloorToInt(GameProgress.Instance.Res(t)).ToString("N0"), 28);
|
||||
UIKit.Size(txt, -1, -1, 1);
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
if (timer != null) timer.text = "Лимиты обновятся через " + GameData.FormatTime(GameProgress.SecondsToNextDay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b67875d9e05857c4eae2c30b9edbef77
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// ВОЙСКА: армия, очередь обучения (идёт и когда игра закрыта), найм воинов за ресурсы,
|
||||
// ускорение за кристаллы. Новые типы войск открываются уровнем Ратуши.
|
||||
public class TroopsPanel : BasePanel
|
||||
{
|
||||
readonly List<Image> queueBars = new List<Image>();
|
||||
readonly List<TextMeshProUGUI> queueTimes = new List<TextMeshProUGUI>();
|
||||
Button speedUp;
|
||||
|
||||
static readonly int[] Amounts = { 1, 10, 50 };
|
||||
|
||||
protected override void Build(RectTransform root)
|
||||
{
|
||||
var d = GP.Data;
|
||||
queueBars.Clear(); queueTimes.Clear(); speedUp = null;
|
||||
var list = ScrollList(root);
|
||||
|
||||
// --- Армия ---
|
||||
var army = UIKit.Card(list, UIKit.Theme.CardDark);
|
||||
ProfilePanel.Header(army, "Армия");
|
||||
ProfilePanel.KV(army, "Всего воинов в городе", GP.TotalTroops.ToString("N0"));
|
||||
ProfilePanel.KV(army, "В походе на карте мира", WorldSystem.TroopsInMarches.ToString("N0"));
|
||||
ProfilePanel.KV(army, "Сила войск", GP.TroopsPower.ToString("N0"));
|
||||
ProfilePanel.KV(army, "Сила армии с героем", GP.ArmyPower.ToString("N0"));
|
||||
UIKit.Button(army, "Отправить армию на карте мира", UIKit.Theme.Green, () => OpenPanel("World"), -1, 80, 26);
|
||||
|
||||
// --- Нет казарм -- нанимать негде ---
|
||||
if (Barracks.Count == 0)
|
||||
{
|
||||
var warn = UIKit.Card(list, new Color(0.35f, 0.18f, 0.12f));
|
||||
UIKit.Text(warn, "Нанимать войска можно только в Казармах. Постройте их на свободном участке.", 28, UIKit.Theme.Text);
|
||||
UIKit.Button(warn, "ПОСТРОИТЬ КАЗАРМЫ", UIKit.Theme.Green, () =>
|
||||
{
|
||||
if (BuildModeController.Instance != null) BuildModeController.Instance.SetMode(true);
|
||||
else UIKit.Toast("Нажмите кнопку СТРОЙКА");
|
||||
}, -1, 96, 32);
|
||||
}
|
||||
|
||||
// --- Очередь обучения ---
|
||||
var queue = UIKit.Card(list);
|
||||
ProfilePanel.Header(queue, "Обучение (" + d.training.Count + " / " + Barracks.QueueSize + ") • казарм: " + Barracks.Count);
|
||||
if (d.training.Count == 0)
|
||||
{
|
||||
UIKit.Text(queue, "Никто не обучается. Выберите войска ниже.", 26, UIKit.Theme.TextDim);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var o in d.training)
|
||||
{
|
||||
var def = GameData.Troops[o.troopIndex];
|
||||
var r = UIKit.Row(queue, 44, 10);
|
||||
var n = UIKit.Text(r, def.name + " × " + o.count, 28);
|
||||
UIKit.Size(n, -1, -1, 1);
|
||||
var t = UIKit.Text(r, "", 26, UIKit.Theme.Gold, TextAlignmentOptions.Right);
|
||||
UIKit.Size(t, 180);
|
||||
queueTimes.Add(t);
|
||||
queueBars.Add(UIKit.Bar(queue, 0, def.color, 20));
|
||||
}
|
||||
speedUp = UIKit.Button(queue, "", UIKit.Theme.Blue, () => GP.SpeedUpTraining(), -1, 80);
|
||||
}
|
||||
Tick();
|
||||
|
||||
// --- Типы войск ---
|
||||
for (int i = 0; i < GameData.Troops.Count; i++)
|
||||
{
|
||||
int idx = i;
|
||||
var def = GameData.Troops[i];
|
||||
bool unlocked = GP.TroopUnlocked(i);
|
||||
|
||||
var card = UIKit.Card(list, unlocked ? UIKit.Theme.Card : UIKit.Theme.CardDark);
|
||||
var top = UIKit.Row(card, 120, 16);
|
||||
UIKit.Icon(top, unlocked ? def.color : UIKit.Theme.Gray, def.name.Substring(0, 1), 110);
|
||||
var info = UIKit.Card(top, new Color(0, 0, 0, 0), 0, 2);
|
||||
UIKit.Size(info, -1, -1, 1);
|
||||
UIKit.Text(info, def.name + " <color=#c8c8c8><size=24>в армии: " + d.troops[i].ToString("N0") + "</size></color>", 32).fontStyle = FontStyles.Bold;
|
||||
UIKit.Text(info, def.description, 22, UIKit.Theme.TextDim);
|
||||
UIKit.Text(info, "Сила " + def.power + " • " + def.secondsPerUnit + " сек/воин • " + GameData.RewardsText(def.cost), 22, UIKit.Theme.TextDim);
|
||||
|
||||
if (!unlocked)
|
||||
{
|
||||
UIKit.Text(card, "Откроется на Ратуше " + def.requiredTownHall + " уровня", 26, UIKit.Theme.Gold, TextAlignmentOptions.Center);
|
||||
continue;
|
||||
}
|
||||
|
||||
var btns = UIKit.Row(card, 80, 10);
|
||||
foreach (int amount in Amounts)
|
||||
{
|
||||
int a = amount;
|
||||
bool can = Barracks.Count > 0 && GP.CanAfford(def.cost, a) && d.training.Count < Barracks.QueueSize;
|
||||
var b = UIKit.Button(btns, "+" + a, can ? UIKit.Theme.Green : UIKit.Theme.Gray, () => GP.Train(idx, a), -1, 80, 30);
|
||||
UIKit.Size(b, -1, 80, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
var d = GP.Data;
|
||||
for (int i = 0; i < queueBars.Count && i < d.training.Count; i++)
|
||||
{
|
||||
var o = d.training[i];
|
||||
UIKit.SetBar(queueBars[i], GP.TrainingProgress(o));
|
||||
queueTimes[i].text = GameData.FormatTime(GP.TrainingSecondsLeft(o));
|
||||
}
|
||||
if (speedUp != null && d.training.Count > 0)
|
||||
UIKit.SetButton(speedUp, "Ускорить всё за " + GP.SpeedUpCost + " кристаллов", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6372f8b3f4381ac44b9000f7d11bb95e
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0c8f3b4e01d3314d8db01fcd150ec37
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,101 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Общая основа всех панелей HUD.
|
||||
// Каждая панель (Profile, Quests, Shop...) наследуется от BasePanel и реализует Build():
|
||||
// "нарисуй своё содержимое в root". Перерисовка происходит сама:
|
||||
// - при открытии панели,
|
||||
// - когда GameProgress сообщает об изменениях (получил награду, обучил войска и т.д.).
|
||||
// Tick() вызывается 2 раза в секунду -- для таймеров, без полной перерисовки.
|
||||
public abstract class BasePanel : MonoBehaviour
|
||||
{
|
||||
protected GameProgress GP { get { return GameProgress.Instance; } }
|
||||
|
||||
RectTransform content;
|
||||
ScrollRect scroll;
|
||||
bool needRebuild;
|
||||
bool resetScroll;
|
||||
float nextTick;
|
||||
|
||||
// false -- панель НЕ перерисовывается от чужих изменений (нужно для ползунков в Настройках)
|
||||
protected virtual bool RebuildOnChange { get { return true; } }
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (RebuildOnChange) GP.OnChanged += RequestRebuild;
|
||||
needRebuild = true;
|
||||
resetScroll = true;
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
if (GameProgress.Instance != null) GameProgress.Instance.OnChanged -= RequestRebuild;
|
||||
}
|
||||
|
||||
public void RequestRebuild() { needRebuild = true; }
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (needRebuild)
|
||||
{
|
||||
needRebuild = false;
|
||||
Rebuild();
|
||||
}
|
||||
if (Time.unscaledTime >= nextTick)
|
||||
{
|
||||
nextTick = Time.unscaledTime + 0.5f;
|
||||
Tick();
|
||||
}
|
||||
Frame();
|
||||
}
|
||||
|
||||
RectTransform Content
|
||||
{
|
||||
get
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
var t = transform.Find("Content");
|
||||
if (t == null)
|
||||
{
|
||||
// запасной вариант для старых панелей без Content
|
||||
var body = transform.Find("Body");
|
||||
if (body != null) body.gameObject.SetActive(false);
|
||||
t = UIKit.Stretch(UIKit.Make("Content", transform), 24, 130, 24, 24);
|
||||
}
|
||||
content = (RectTransform)t;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
void Rebuild()
|
||||
{
|
||||
float pos = (scroll != null && !resetScroll) ? scroll.verticalNormalizedPosition : 1f;
|
||||
resetScroll = false;
|
||||
scroll = null;
|
||||
UIKit.Clear(Content);
|
||||
Build(Content);
|
||||
if (scroll != null)
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
scroll.verticalNormalizedPosition = pos;
|
||||
}
|
||||
}
|
||||
|
||||
// Прокручиваемый список на всю панель (позиция прокрутки сохраняется между перерисовками)
|
||||
protected RectTransform ScrollList(Transform parent, float spacing = 14)
|
||||
{
|
||||
return UIKit.ScrollList(parent, out scroll, spacing);
|
||||
}
|
||||
|
||||
protected abstract void Build(RectTransform root);
|
||||
protected virtual void Tick() { }
|
||||
protected virtual void Frame() { } // каждый кадр -- для плавного движения (армии на карте)
|
||||
|
||||
// Открыть другую панель (например, из "Боя" перейти в "Войска")
|
||||
protected void OpenPanel(string id)
|
||||
{
|
||||
if (UIPanelManager.Instance != null) UIPanelManager.Instance.Open(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ce45e75df7b9fe49b3246ce676ac33a
|
||||
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// Красный кружок с числом на кнопке HUD: "есть что забрать".
|
||||
// Вешается билдером на кнопки (Задания, Почта, Награды...). panelId = id панели.
|
||||
public class HUDBadge : MonoBehaviour
|
||||
{
|
||||
public string panelId;
|
||||
|
||||
GameObject badge;
|
||||
TextMeshProUGUI label;
|
||||
float next;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Если на кнопке уже есть старый значок-заглушка из скелета -- прячем его
|
||||
var old = transform.Find("CountBadge");
|
||||
if (old != null) old.gameObject.SetActive(false);
|
||||
|
||||
var rt = UIKit.Make("HUDBadge", transform);
|
||||
rt.anchorMin = rt.anchorMax = new Vector2(1, 1);
|
||||
rt.pivot = new Vector2(1, 1);
|
||||
rt.sizeDelta = new Vector2(44, 44);
|
||||
rt.anchoredPosition = new Vector2(4, 4);
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = new Color(0.9f, 0.15f, 0.15f);
|
||||
img.raycastTarget = false;
|
||||
label = UIKit.Text(rt, "", 26, Color.white, TextAlignmentOptions.Center);
|
||||
UIKit.Stretch(label.rectTransform);
|
||||
label.fontStyle = FontStyles.Bold;
|
||||
badge = rt.gameObject;
|
||||
badge.SetActive(false);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (badge == null || Time.unscaledTime < next) return;
|
||||
next = Time.unscaledTime + 0.5f;
|
||||
int n = GameProgress.Instance.BadgeCount(panelId);
|
||||
badge.SetActive(n > 0);
|
||||
label.text = n > 9 ? "9+" : n.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 702c55f5c56bb7d48aa8b28f51947acf
|
||||
@@ -0,0 +1,410 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
// Конструктор интерфейса из кода: текст, кнопки, строки, прокрутка, полоски прогресса,
|
||||
// всплывающие сообщения (Toast) и окна (подтверждение, ввод имени, результат боя).
|
||||
//
|
||||
// Почему из кода: списки (задания, письма, товары) меняются во время игры, их всё равно
|
||||
// надо создавать на лету. Цвета и размеры -- в Theme ниже, дизайнер может поменять их тут.
|
||||
public static class UIKit
|
||||
{
|
||||
// ---------------- ТЕМА ----------------
|
||||
public static class Theme
|
||||
{
|
||||
public static readonly Color Card = new Color(0.17f, 0.19f, 0.24f, 1f);
|
||||
public static readonly Color CardDark = new Color(0.12f, 0.13f, 0.17f, 1f);
|
||||
public static readonly Color CardDone = new Color(0.16f, 0.26f, 0.18f, 1f);
|
||||
public static readonly Color Gold = new Color(0.95f, 0.75f, 0.25f, 1f);
|
||||
public static readonly Color Green = new Color(0.25f, 0.62f, 0.3f, 1f);
|
||||
public static readonly Color Red = new Color(0.72f, 0.24f, 0.22f, 1f);
|
||||
public static readonly Color Blue = new Color(0.25f, 0.45f, 0.75f, 1f);
|
||||
public static readonly Color Gray = new Color(0.32f, 0.33f, 0.37f, 1f);
|
||||
public static readonly Color Text = new Color(0.95f, 0.95f, 0.95f, 1f);
|
||||
public static readonly Color TextDim = new Color(0.7f, 0.72f, 0.78f, 1f);
|
||||
public static readonly Color BarBg = new Color(0.08f, 0.08f, 0.1f, 1f);
|
||||
}
|
||||
|
||||
// ---------------- БАЗА ----------------
|
||||
|
||||
public static RectTransform Make(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
go.transform.SetParent(parent, false);
|
||||
return (RectTransform)go.transform;
|
||||
}
|
||||
|
||||
public static RectTransform Stretch(RectTransform rt, float left = 0, float top = 0, float right = 0, float bottom = 0)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = new Vector2(left, bottom);
|
||||
rt.offsetMax = new Vector2(-right, -top);
|
||||
return rt;
|
||||
}
|
||||
|
||||
public static LayoutElement Size(Component c, float width = -1, float height = -1, float flexW = -1)
|
||||
{
|
||||
var le = c.GetComponent<LayoutElement>();
|
||||
if (le == null) le = c.gameObject.AddComponent<LayoutElement>();
|
||||
if (width >= 0) { le.preferredWidth = width; le.minWidth = width; }
|
||||
if (height >= 0) { le.preferredHeight = height; le.minHeight = height; }
|
||||
if (flexW >= 0) le.flexibleWidth = flexW;
|
||||
return le;
|
||||
}
|
||||
|
||||
public static void Clear(Transform t)
|
||||
{
|
||||
for (int i = t.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var c = t.GetChild(i).gameObject;
|
||||
c.SetActive(false);
|
||||
UnityEngine.Object.Destroy(c);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- ЭЛЕМЕНТЫ ----------------
|
||||
|
||||
public static Image Box(Transform parent, Color color, float height = -1)
|
||||
{
|
||||
var rt = Make("Box", parent);
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = color;
|
||||
if (height >= 0) Size(img, -1, height);
|
||||
return img;
|
||||
}
|
||||
|
||||
public static TextMeshProUGUI Text(Transform parent, string text, float size = 30, Color? color = null,
|
||||
TextAlignmentOptions align = TextAlignmentOptions.Left, float height = -1)
|
||||
{
|
||||
var rt = Make("Text", parent);
|
||||
var t = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
t.text = text;
|
||||
t.fontSize = size;
|
||||
t.color = color ?? Theme.Text;
|
||||
t.alignment = align;
|
||||
t.raycastTarget = false;
|
||||
t.richText = true;
|
||||
if (height >= 0) Size(t, -1, height);
|
||||
return t;
|
||||
}
|
||||
|
||||
public static Button Button(Transform parent, string label, Color color, Action onClick,
|
||||
float width = -1, float height = 86, float fontSize = 28)
|
||||
{
|
||||
var rt = Make("Btn_" + label, parent);
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = color;
|
||||
var b = rt.gameObject.AddComponent<Button>();
|
||||
var colors = b.colors;
|
||||
colors.disabledColor = new Color(0.55f, 0.55f, 0.55f, 0.6f);
|
||||
b.colors = colors;
|
||||
if (onClick != null) b.onClick.AddListener(() => onClick());
|
||||
var t = Text(rt, label, fontSize, Theme.Text, TextAlignmentOptions.Center);
|
||||
Stretch(t.rectTransform, 8, 4, 8, 4);
|
||||
t.enableAutoSizing = true; t.fontSizeMin = 16; t.fontSizeMax = fontSize;
|
||||
Size(b, width, height);
|
||||
return b;
|
||||
}
|
||||
|
||||
public static void SetButton(Button b, string label, bool interactable)
|
||||
{
|
||||
if (b == null) return;
|
||||
var t = b.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (t != null && label != null) t.text = label;
|
||||
b.interactable = interactable;
|
||||
}
|
||||
|
||||
// Горизонтальная строка: элементы слева направо
|
||||
public static RectTransform Row(Transform parent, float height = 90, float spacing = 12, Color? bg = null, int padding = 0)
|
||||
{
|
||||
var rt = Make("Row", parent);
|
||||
if (bg.HasValue) rt.gameObject.AddComponent<Image>().color = bg.Value;
|
||||
var h = rt.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = spacing;
|
||||
h.padding = new RectOffset(padding, padding, padding, padding);
|
||||
h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childControlWidth = true; h.childControlHeight = true;
|
||||
h.childForceExpandWidth = false; h.childForceExpandHeight = true;
|
||||
if (height >= 0) Size(rt, -1, height);
|
||||
return rt;
|
||||
}
|
||||
|
||||
// Вертикальная "карточка": элементы сверху вниз, высота по содержимому
|
||||
public static RectTransform Card(Transform parent, Color? bg = null, int padding = 18, float spacing = 10)
|
||||
{
|
||||
var rt = Make("Card", parent);
|
||||
rt.gameObject.AddComponent<Image>().color = bg ?? Theme.Card;
|
||||
var v = rt.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
v.padding = new RectOffset(padding, padding, padding, padding);
|
||||
v.spacing = spacing;
|
||||
v.childControlWidth = true; v.childControlHeight = true;
|
||||
v.childForceExpandWidth = true; v.childForceExpandHeight = false;
|
||||
return rt;
|
||||
}
|
||||
|
||||
public static RectTransform Spacer(Transform parent, float height)
|
||||
{
|
||||
var rt = Make("Spacer", parent);
|
||||
Size(rt, -1, height);
|
||||
return rt;
|
||||
}
|
||||
|
||||
// Прокручиваемый список на всю область parent. Возвращает контейнер для элементов.
|
||||
public static RectTransform ScrollList(Transform parent, out ScrollRect scroll, float spacing = 14)
|
||||
{
|
||||
var root = Stretch(Make("Scroll", parent));
|
||||
var bg = root.gameObject.AddComponent<Image>();
|
||||
bg.color = new Color(0, 0, 0, 0); // прозрачный, но ловит свайпы
|
||||
scroll = root.gameObject.AddComponent<ScrollRect>();
|
||||
scroll.horizontal = false;
|
||||
scroll.movementType = ScrollRect.MovementType.Elastic;
|
||||
scroll.scrollSensitivity = 40f;
|
||||
|
||||
var viewport = Stretch(Make("Viewport", root));
|
||||
viewport.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var content = Make("Content", viewport);
|
||||
content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1);
|
||||
content.pivot = new Vector2(0.5f, 1);
|
||||
content.offsetMin = Vector2.zero; content.offsetMax = Vector2.zero;
|
||||
var v = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
v.spacing = spacing;
|
||||
v.padding = new RectOffset(6, 6, 6, 24);
|
||||
v.childControlWidth = true; v.childControlHeight = true;
|
||||
v.childForceExpandWidth = true; v.childForceExpandHeight = false;
|
||||
var fit = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
scroll.viewport = viewport;
|
||||
scroll.content = content;
|
||||
return content;
|
||||
}
|
||||
|
||||
// Полоска прогресса. Возвращает картинку заливки (для обновления через SetBar)
|
||||
public static Image Bar(Transform parent, float fraction, Color fill, float height = 26, string label = null)
|
||||
{
|
||||
var bg = Box(parent, Theme.BarBg, height);
|
||||
var f = Make("Fill", bg.transform);
|
||||
var img = f.gameObject.AddComponent<Image>();
|
||||
img.color = fill;
|
||||
img.raycastTarget = false;
|
||||
f.anchorMin = Vector2.zero; f.offsetMin = Vector2.zero; f.offsetMax = Vector2.zero;
|
||||
SetBar(img, fraction);
|
||||
if (label != null)
|
||||
{
|
||||
var t = Text(bg.transform, label, height * 0.7f, Theme.Text, TextAlignmentOptions.Center);
|
||||
Stretch(t.rectTransform);
|
||||
t.name = "Label";
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
public static void SetBar(Image fill, float fraction, string label = null)
|
||||
{
|
||||
if (fill == null) return;
|
||||
var rt = fill.rectTransform;
|
||||
rt.anchorMax = new Vector2(Mathf.Clamp01(fraction), 1);
|
||||
if (label != null)
|
||||
{
|
||||
var t = fill.transform.parent.Find("Label");
|
||||
if (t != null) t.GetComponent<TextMeshProUGUI>().text = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Цветной квадрат-иконка (заглушка, пока нет спрайтов) с буквой
|
||||
public static Image Icon(Transform parent, Color color, string letter, float size = 80)
|
||||
{
|
||||
var img = Box(parent, color);
|
||||
Size(img, size, size);
|
||||
if (!string.IsNullOrEmpty(letter))
|
||||
{
|
||||
var t = Text(img.transform, letter, size * 0.5f, Color.white, TextAlignmentOptions.Center);
|
||||
Stretch(t.rectTransform);
|
||||
t.fontStyle = FontStyles.Bold;
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
public static Slider Slider(Transform parent, float value, Action<float> onChange)
|
||||
{
|
||||
var go = DefaultControls.CreateSlider(new DefaultControls.Resources());
|
||||
go.transform.SetParent(parent, false);
|
||||
var s = go.GetComponent<Slider>();
|
||||
s.minValue = 0; s.maxValue = 1; s.value = value;
|
||||
foreach (var img in go.GetComponentsInChildren<Image>())
|
||||
{
|
||||
if (img.name == "Background") img.color = Theme.BarBg;
|
||||
else if (img.name == "Fill") img.color = Theme.Gold;
|
||||
else if (img.name == "Handle") img.color = Color.white;
|
||||
}
|
||||
if (onChange != null) s.onValueChanged.AddListener(v => onChange(v));
|
||||
Size(s, -1, 50, 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Кнопка-переключатель ВКЛ/ВЫКЛ
|
||||
public static Button Switch(Transform parent, bool on, Action<bool> onChange)
|
||||
{
|
||||
Button b = null;
|
||||
bool state = on;
|
||||
b = Button(parent, on ? "ВКЛ" : "ВЫКЛ", on ? Theme.Green : Theme.Gray, () =>
|
||||
{
|
||||
state = !state;
|
||||
b.GetComponent<Image>().color = state ? Theme.Green : Theme.Gray;
|
||||
b.GetComponentInChildren<TextMeshProUGUI>().text = state ? "ВКЛ" : "ВЫКЛ";
|
||||
if (onChange != null) onChange(state);
|
||||
}, 160, 70);
|
||||
return b;
|
||||
}
|
||||
|
||||
// Вкладки: ряд кнопок, текущая подсвечена
|
||||
public static void Tabs(Transform parent, string[] names, int current, Action<int> onSelect)
|
||||
{
|
||||
var row = Row(parent, 80, 8);
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
int idx = i;
|
||||
var b = Button(row, names[i], i == current ? Theme.Gold : Theme.CardDark, () => onSelect(idx), -1, 80, 28);
|
||||
Size(b, -1, 80, 1);
|
||||
if (i == current) b.GetComponentInChildren<TextMeshProUGUI>().color = new Color(0.15f, 0.1f, 0.05f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- ВЕРХНИЙ СЛОЙ (тосты и окна) ----------------
|
||||
|
||||
static RectTransform overlay;
|
||||
|
||||
public static RectTransform Overlay
|
||||
{
|
||||
get
|
||||
{
|
||||
if (overlay == null)
|
||||
{
|
||||
var go = new GameObject("UI_Overlay", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = go.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 1000;
|
||||
var scaler = go.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1080, 1920);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
overlay = (RectTransform)go.transform;
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
}
|
||||
|
||||
static ToastRunner runner;
|
||||
static ToastRunner Runner
|
||||
{
|
||||
get
|
||||
{
|
||||
if (runner == null) runner = Overlay.gameObject.AddComponent<ToastRunner>();
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
|
||||
// Всплывающее сообщение внизу экрана на 2.5 секунды
|
||||
public static void Toast(string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message)) return;
|
||||
if (!Application.isPlaying) { Debug.Log(message); return; }
|
||||
Runner.Show(message);
|
||||
}
|
||||
|
||||
class ToastRunner : MonoBehaviour
|
||||
{
|
||||
int active;
|
||||
public void Show(string message) { StartCoroutine(Run(message)); }
|
||||
|
||||
IEnumerator Run(string message)
|
||||
{
|
||||
var rt = Make("Toast", Overlay);
|
||||
rt.anchorMin = new Vector2(0.08f, 0); rt.anchorMax = new Vector2(0.92f, 0);
|
||||
rt.pivot = new Vector2(0.5f, 0);
|
||||
rt.anchoredPosition = new Vector2(0, 300 + active * 110);
|
||||
rt.sizeDelta = new Vector2(0, 96);
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = new Color(0.05f, 0.05f, 0.07f, 0.92f);
|
||||
img.raycastTarget = false;
|
||||
var t = Text(rt, message, 28, Theme.Text, TextAlignmentOptions.Center);
|
||||
Stretch(t.rectTransform, 20, 6, 20, 6);
|
||||
t.enableAutoSizing = true; t.fontSizeMin = 16; t.fontSizeMax = 28;
|
||||
var group = rt.gameObject.AddComponent<CanvasGroup>();
|
||||
group.blocksRaycasts = false;
|
||||
active++;
|
||||
yield return new WaitForSecondsRealtime(2.2f);
|
||||
for (float a = 1; a > 0; a -= Time.unscaledDeltaTime * 3f) { group.alpha = a; yield return null; }
|
||||
active--;
|
||||
Destroy(rt.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// Окно поверх всего: затемнение + карточка по центру. Возвращает карточку для наполнения.
|
||||
public static RectTransform Modal(string title, out GameObject root)
|
||||
{
|
||||
var dim = Stretch(Make("Modal", Overlay));
|
||||
dim.gameObject.AddComponent<Image>().color = new Color(0, 0, 0, 0.65f);
|
||||
root = dim.gameObject;
|
||||
|
||||
var card = Card(dim, Theme.CardDark, 32, 20);
|
||||
card.anchorMin = new Vector2(0.06f, 0.5f); card.anchorMax = new Vector2(0.94f, 0.5f);
|
||||
card.pivot = new Vector2(0.5f, 0.5f);
|
||||
card.anchoredPosition = Vector2.zero;
|
||||
var fit = card.gameObject.AddComponent<ContentSizeFitter>();
|
||||
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
var t = Text(card, title, 42, Theme.Gold, TextAlignmentOptions.Center);
|
||||
t.fontStyle = FontStyles.Bold;
|
||||
return card;
|
||||
}
|
||||
|
||||
public static void Message(string title, string text, string button = "ОК", Action onClose = null)
|
||||
{
|
||||
GameObject root;
|
||||
var card = Modal(title, out root);
|
||||
Text(card, text, 30, Theme.Text, TextAlignmentOptions.Center);
|
||||
Spacer(card, 10);
|
||||
Button(card, button, Theme.Green, () => { UnityEngine.Object.Destroy(root); if (onClose != null) onClose(); }, -1, 96, 32);
|
||||
}
|
||||
|
||||
public static void Confirm(string title, string text, string yes, Action onYes)
|
||||
{
|
||||
GameObject root;
|
||||
var card = Modal(title, out root);
|
||||
Text(card, text, 30, Theme.Text, TextAlignmentOptions.Center);
|
||||
var row = Row(card, 96, 20);
|
||||
var no = Button(row, "Отмена", Theme.Gray, () => UnityEngine.Object.Destroy(root));
|
||||
Size(no, -1, 96, 1);
|
||||
var ok = Button(row, yes, Theme.Red, () => { UnityEngine.Object.Destroy(root); if (onYes != null) onYes(); });
|
||||
Size(ok, -1, 96, 1);
|
||||
}
|
||||
|
||||
public static void InputDialog(string title, string initial, int maxLength, Action<string> onOk)
|
||||
{
|
||||
GameObject root;
|
||||
var card = Modal(title, out root);
|
||||
var go = TMP_DefaultControls.CreateInputField(new TMP_DefaultControls.Resources());
|
||||
go.transform.SetParent(card, false);
|
||||
var input = go.GetComponent<TMP_InputField>();
|
||||
input.text = initial;
|
||||
input.characterLimit = maxLength;
|
||||
input.pointSize = 34;
|
||||
go.GetComponent<Image>().color = new Color(0.9f, 0.9f, 0.92f);
|
||||
Size(input, -1, 90);
|
||||
|
||||
var row = Row(card, 96, 20);
|
||||
var no = Button(row, "Отмена", Theme.Gray, () => UnityEngine.Object.Destroy(root));
|
||||
Size(no, -1, 96, 1);
|
||||
var ok = Button(row, "Сохранить", Theme.Green, () =>
|
||||
{
|
||||
string v = input.text.Trim();
|
||||
if (v.Length == 0) { Toast("Имя не может быть пустым"); return; }
|
||||
UnityEngine.Object.Destroy(root);
|
||||
if (onOk != null) onOk(v);
|
||||
});
|
||||
Size(ok, -1, 96, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a737e5709aa73d4da0d72f02009e6e8
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29c363642fd8d494cb9127032e7e2083
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// ГЛОБАЛЬНАЯ КАРТА: таблицы. Характеристики юнитов, поселения на карте, скорость походов.
|
||||
// Баланс правится здесь. Логика -- в WorldSystem.cs, отображение -- в WorldMapPanel.cs.
|
||||
|
||||
// Боевые характеристики одного воина (индексы совпадают с GameData.Troops)
|
||||
public class UnitStats
|
||||
{
|
||||
public int hp; // здоровье
|
||||
public int attack; // урон за раунд
|
||||
public int defense; // снижает получаемый урон (каждая единица = -8%)
|
||||
public float speed; // скорость на карте (1 = обычная). Армия идёт со скоростью самого медленного
|
||||
public int carry; // сколько ресурсов уносит один воин
|
||||
|
||||
public UnitStats(int hp, int attack, int defense, float speed, int carry)
|
||||
{
|
||||
this.hp = hp; this.attack = attack; this.defense = defense; this.speed = speed; this.carry = carry;
|
||||
}
|
||||
}
|
||||
|
||||
// Поселение на карте
|
||||
public class SettlementDef
|
||||
{
|
||||
public string id;
|
||||
public string name;
|
||||
public Vector2 pos; // позиция на карте, 0..1 по ширине и высоте
|
||||
public bool isHome; // город игрока
|
||||
public int[] baseGarrison; // защитники на 1 уровне (по типам войск)
|
||||
public RewardItem[] baseLoot; // что можно унести на 1 уровне
|
||||
public float respawnMinutes; // через сколько поселение отстраивается после разграбления
|
||||
public Color color;
|
||||
|
||||
public SettlementDef(string id, string name, float x, float y, Color color)
|
||||
{
|
||||
this.id = id; this.name = name; pos = new Vector2(x, y); this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorldData
|
||||
{
|
||||
// Индексы: 0 Пехота, 1 Лучники, 2 Кавалерия, 3 Осадные
|
||||
public static readonly UnitStats[] Units =
|
||||
{
|
||||
new UnitStats(hp: 10, attack: 3, defense: 3, speed: 1.0f, carry: 10),
|
||||
new UnitStats(hp: 7, attack: 5, defense: 1, speed: 1.0f, carry: 8),
|
||||
new UnitStats(hp: 14, attack: 6, defense: 2, speed: 1.6f, carry: 15),
|
||||
new UnitStats(hp: 20, attack: 12, defense: 1, speed: 0.6f, carry: 5),
|
||||
};
|
||||
|
||||
// Сколько секунд идёт армия со скоростью 1 через всю карту (расстояние 1.0).
|
||||
// Для тестов маленькое -- поход туда-обратно занимает около минуты-двух.
|
||||
public const float SecondsPerMapUnit = 90f;
|
||||
|
||||
public const int MaxMarches = 2; // одновременно в походе
|
||||
public const int MaxRounds = 10; // раундов в бою, потом атакующий отступает
|
||||
public const float DefenderBonus = 0.1f; // защитники бьют на 10% сильнее (стены)
|
||||
public const float GarrisonRegenMinutes = 3f; // недобитый гарнизон восстанавливается
|
||||
|
||||
public static readonly List<SettlementDef> Settlements = Build();
|
||||
|
||||
static List<SettlementDef> Build()
|
||||
{
|
||||
var home = new SettlementDef("home", "Ваш город", 0.18f, 0.5f, new Color(0.25f, 0.5f, 0.9f));
|
||||
home.isHome = true;
|
||||
|
||||
var bandits = new SettlementDef("bandits", "Деревня разбойников", 0.55f, 0.25f, new Color(0.75f, 0.55f, 0.25f));
|
||||
bandits.baseGarrison = new[] { 8, 4, 0, 0 };
|
||||
bandits.baseLoot = new[] { new RewardItem(ResourceType.Wood, 400), new RewardItem(ResourceType.Food, 400), new RewardItem(ResourceType.Gold, 150) };
|
||||
bandits.respawnMinutes = 3f;
|
||||
|
||||
var fortress = new SettlementDef("fortress", "Крепость варваров", 0.82f, 0.72f, new Color(0.75f, 0.25f, 0.25f));
|
||||
fortress.baseGarrison = new[] { 25, 15, 8, 2 };
|
||||
fortress.baseLoot = new[] { new RewardItem(ResourceType.Stone, 900), new RewardItem(ResourceType.Iron, 600), new RewardItem(ResourceType.Gold, 500), new RewardItem(ResourceType.Gems, 20) };
|
||||
fortress.respawnMinutes = 10f;
|
||||
|
||||
return new List<SettlementDef> { home, bandits, fortress };
|
||||
}
|
||||
|
||||
public static SettlementDef Get(string id) { return Settlements.Find(s => s.id == id); }
|
||||
public static SettlementDef Home { get { return Settlements[0]; } }
|
||||
|
||||
// С каждым разграблением поселение отстраивается на уровень выше: больше защитников и добычи
|
||||
public static float LevelMultiplier(int level) { return 1f + 0.5f * (level - 1); }
|
||||
|
||||
public static int UnitPower(int index) { return GameData.Troops[index].power; }
|
||||
}
|
||||
|
||||
// --- Сохраняемое состояние (часть ProgressData) ---
|
||||
|
||||
[Serializable]
|
||||
public class SettlementState
|
||||
{
|
||||
public string id;
|
||||
public int level = 1;
|
||||
public int[] garrison = new int[4]; // текущие защитники
|
||||
public long ruinedUntilTicks; // > сейчас -- поселение разграблено и пустует
|
||||
public long lastBattleTicks;
|
||||
public int timesRaided;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WorldMarch
|
||||
{
|
||||
public int id;
|
||||
public string targetId;
|
||||
public int[] units = new int[4]; // отправлено
|
||||
public int[] survivors = new int[4]; // вернутся домой
|
||||
public long startTicks;
|
||||
public long arriveTicks;
|
||||
public long returnTicks;
|
||||
public bool returning;
|
||||
public bool recalled;
|
||||
public float turnPoint = 1f; // где развернулись: 1 = у цели, меньше -- отозвали по дороге
|
||||
public List<RewardItem> loot = new List<RewardItem>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class BattleReport
|
||||
{
|
||||
public long ticks;
|
||||
public string title;
|
||||
public string text;
|
||||
public bool win;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb78904d07b16a64682697f8a240feb5
|
||||
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
// Линия-маршрут на карте между двумя точками (координаты 0..1 внутри родителя).
|
||||
// Пересчитывается каждый кадр, поэтому не зависит от размера экрана.
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class WorldLine : MonoBehaviour
|
||||
{
|
||||
public Vector2 from;
|
||||
public Vector2 to;
|
||||
public float thickness = 6f;
|
||||
|
||||
RectTransform rt;
|
||||
RectTransform parent;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
rt = (RectTransform)transform;
|
||||
parent = (RectTransform)transform.parent;
|
||||
rt.pivot = new Vector2(0f, 0.5f);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (parent == null) return;
|
||||
Vector2 size = parent.rect.size;
|
||||
Vector2 a = Vector2.Scale(from, size);
|
||||
Vector2 b = Vector2.Scale(to, size);
|
||||
Vector2 d = b - a;
|
||||
rt.anchorMin = rt.anchorMax = from;
|
||||
rt.anchoredPosition = Vector2.zero;
|
||||
rt.sizeDelta = new Vector2(d.magnitude, thickness);
|
||||
rt.localRotation = Quaternion.Euler(0, 0, Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dda3d4605bc23a4fa4655209a6c30ba
|
||||
@@ -0,0 +1,383 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c8463f829cbc0840b0b660e08974aa6
|
||||
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
// ЛОГИКА ГЛОБАЛЬНОЙ КАРТЫ:
|
||||
// отправка армии -> марш по маршруту (таймер) -> прибытие -> бой по раундам -> потери, добыча
|
||||
// -> возвращение домой (таймер) -> выжившие снова в городе, добыча на складе, отчёт в почте.
|
||||
// Время реальное (как у обучения войск): поход продолжается, даже если игра закрыта.
|
||||
public static class WorldSystem
|
||||
{
|
||||
static GameProgress GP { get { return GameProgress.Instance; } }
|
||||
static ProgressData D { get { return GameProgress.Instance.Data; } }
|
||||
static long Now { get { return DateTime.UtcNow.Ticks; } }
|
||||
|
||||
// ================= ПОСЕЛЕНИЯ =================
|
||||
|
||||
public static SettlementState State(string id)
|
||||
{
|
||||
var s = D.settlements.Find(x => x.id == id);
|
||||
if (s == null)
|
||||
{
|
||||
s = new SettlementState { id = id };
|
||||
var def = WorldData.Get(id);
|
||||
if (def != null && def.baseGarrison != null) s.garrison = FullGarrison(def, 1);
|
||||
D.settlements.Add(s);
|
||||
}
|
||||
if (s.garrison == null || s.garrison.Length < 4) s.garrison = new int[4];
|
||||
return s;
|
||||
}
|
||||
|
||||
public static int[] FullGarrison(SettlementDef def, int level)
|
||||
{
|
||||
var g = new int[4];
|
||||
if (def.baseGarrison == null) return g;
|
||||
float m = WorldData.LevelMultiplier(level);
|
||||
for (int i = 0; i < 4 && i < def.baseGarrison.Length; i++) g[i] = Mathf.RoundToInt(def.baseGarrison[i] * m);
|
||||
return g;
|
||||
}
|
||||
|
||||
public static RewardItem[] FullLoot(SettlementDef def, int level)
|
||||
{
|
||||
if (def.baseLoot == null) return new RewardItem[0];
|
||||
float m = WorldData.LevelMultiplier(level);
|
||||
var list = new RewardItem[def.baseLoot.Length];
|
||||
for (int i = 0; i < list.Length; i++) list[i] = new RewardItem(def.baseLoot[i].type, Mathf.RoundToInt(def.baseLoot[i].amount * m));
|
||||
return list;
|
||||
}
|
||||
|
||||
// Разграблено и пока пустует?
|
||||
public static bool IsRuined(string id, long at) { return State(id).ruinedUntilTicks > at; }
|
||||
public static double RuinedSecondsLeft(string id) { return TimeSpan.FromTicks(Math.Max(0, State(id).ruinedUntilTicks - Now)).TotalSeconds; }
|
||||
|
||||
// Восстановление поселений со временем
|
||||
static void RefreshSettlement(SettlementDef def, long at)
|
||||
{
|
||||
if (def.isHome) return;
|
||||
var s = State(def.id);
|
||||
if (s.ruinedUntilTicks > 0 && s.ruinedUntilTicks <= at)
|
||||
{
|
||||
s.ruinedUntilTicks = 0; // отстроились, уже на новом уровне
|
||||
s.garrison = FullGarrison(def, s.level);
|
||||
GP.MarkChanged();
|
||||
}
|
||||
else if (s.ruinedUntilTicks == 0 && s.lastBattleTicks > 0 &&
|
||||
at - s.lastBattleTicks >= TimeSpan.FromMinutes(WorldData.GarrisonRegenMinutes).Ticks)
|
||||
{
|
||||
var full = FullGarrison(def, s.level);
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 4; i++) if (s.garrison[i] != full[i]) { s.garrison[i] = full[i]; changed = true; }
|
||||
s.lastBattleTicks = 0;
|
||||
if (changed) GP.MarkChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public static int GarrisonPower(int[] g)
|
||||
{
|
||||
int p = 0;
|
||||
for (int i = 0; i < 4; i++) p += g[i] * WorldData.UnitPower(i);
|
||||
return p;
|
||||
}
|
||||
|
||||
// ================= МАРШРУТ И ВРЕМЯ =================
|
||||
|
||||
public static float Distance(SettlementDef target) { return Vector2.Distance(WorldData.Home.pos, target.pos); }
|
||||
|
||||
public static float ArmySpeed(int[] units)
|
||||
{
|
||||
float speed = float.MaxValue;
|
||||
for (int i = 0; i < 4; i++) if (units[i] > 0) speed = Mathf.Min(speed, WorldData.Units[i].speed);
|
||||
return speed == float.MaxValue ? 1f : speed;
|
||||
}
|
||||
|
||||
public static double TravelSeconds(SettlementDef target, int[] units)
|
||||
{
|
||||
return Distance(target) * WorldData.SecondsPerMapUnit / ArmySpeed(units);
|
||||
}
|
||||
|
||||
public static int Carry(int[] units)
|
||||
{
|
||||
int c = 0;
|
||||
for (int i = 0; i < 4; i++) c += units[i] * WorldData.Units[i].carry;
|
||||
return c;
|
||||
}
|
||||
|
||||
public static int Count(int[] units) { int n = 0; foreach (var u in units) n += u; return n; }
|
||||
|
||||
public static int Power(int[] units) { return GarrisonPower(units); }
|
||||
|
||||
// Где сейчас армия: 0 = дома, 1 = у цели
|
||||
public static float Progress(WorldMarch m)
|
||||
{
|
||||
long now = Now;
|
||||
if (!m.returning)
|
||||
return Mathf.Clamp01((float)(now - m.startTicks) / Math.Max(1, m.arriveTicks - m.startTicks));
|
||||
// на обратном пути: из точки разворота (1 = от цели, меньше -- если отозвали по дороге) домой
|
||||
float t = Mathf.Clamp01((float)(now - m.arriveTicks) / Math.Max(1, m.returnTicks - m.arriveTicks));
|
||||
return Mathf.Lerp(m.turnPoint, 0f, t);
|
||||
}
|
||||
|
||||
public static double SecondsLeft(WorldMarch m)
|
||||
{
|
||||
long end = m.returning ? m.returnTicks : m.arriveTicks;
|
||||
return TimeSpan.FromTicks(Math.Max(0, end - Now)).TotalSeconds;
|
||||
}
|
||||
|
||||
public static int TroopsInMarches
|
||||
{
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
foreach (var m in D.marches) n += Count(m.returning ? m.survivors : m.units);
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= ОТПРАВКА / ОТЗЫВ =================
|
||||
|
||||
public static string CanSendReason(string targetId, int[] units)
|
||||
{
|
||||
var def = WorldData.Get(targetId);
|
||||
if (def == null || def.isHome) return "Выберите цель";
|
||||
if (D.marches.Count >= WorldData.MaxMarches) return "Все отряды уже в походе (макс. " + WorldData.MaxMarches + ")";
|
||||
if (Count(units) <= 0) return "Выберите войска";
|
||||
for (int i = 0; i < 4; i++) if (units[i] > D.troops[i]) return "Столько войск нет в городе";
|
||||
if (IsRuined(targetId, Now)) return "Поселение разграблено, ещё отстраивается";
|
||||
return null;
|
||||
}
|
||||
|
||||
public static WorldMarch Send(string targetId, int[] units)
|
||||
{
|
||||
string why = CanSendReason(targetId, units);
|
||||
if (why != null) { UIKit.Toast(why); return null; }
|
||||
|
||||
var def = WorldData.Get(targetId);
|
||||
var m = new WorldMarch { id = D.nextMarchId++, targetId = targetId, startTicks = Now };
|
||||
for (int i = 0; i < 4; i++) { m.units[i] = units[i]; D.troops[i] -= units[i]; }
|
||||
m.arriveTicks = m.startTicks + (long)(TravelSeconds(def, units) * TimeSpan.TicksPerSecond);
|
||||
D.marches.Add(m);
|
||||
D.marchesSent++;
|
||||
D.dailyMarches++;
|
||||
UIKit.Toast("Армия выступила: " + def.name + ". Прибытие через " + GameData.FormatTime(SecondsLeft(m)));
|
||||
GP.MarkChanged();
|
||||
return m;
|
||||
}
|
||||
|
||||
// Развернуть армию домой, пока она не дошла (обратно идёт столько же, сколько прошла)
|
||||
public static void Recall(WorldMarch m)
|
||||
{
|
||||
if (m == null || m.returning) return;
|
||||
long now = Now;
|
||||
long walked = now - m.startTicks;
|
||||
m.turnPoint = Progress(m);
|
||||
m.returning = true;
|
||||
m.recalled = true;
|
||||
for (int i = 0; i < 4; i++) m.survivors[i] = m.units[i];
|
||||
m.arriveTicks = now;
|
||||
m.returnTicks = now + walked;
|
||||
UIKit.Toast("Армия разворачивается домой");
|
||||
GP.MarkChanged();
|
||||
}
|
||||
|
||||
// ================= ОБРАБОТКА (вызывается раз в секунду из GameProgress) =================
|
||||
|
||||
public static void Process()
|
||||
{
|
||||
long now = Now;
|
||||
foreach (var def in WorldData.Settlements) RefreshSettlement(def, now);
|
||||
|
||||
// копия списка: внутри можем удалять
|
||||
foreach (var m in D.marches.ToArray())
|
||||
{
|
||||
if (!m.returning && now >= m.arriveTicks) Arrive(m);
|
||||
if (m.returning && now >= m.returnTicks) ReturnHome(m);
|
||||
}
|
||||
}
|
||||
|
||||
static void Arrive(WorldMarch m)
|
||||
{
|
||||
var def = WorldData.Get(m.targetId);
|
||||
RefreshSettlement(def, m.arriveTicks);
|
||||
var s = State(m.targetId);
|
||||
long travel = m.arriveTicks - m.startTicks;
|
||||
|
||||
if (s.ruinedUntilTicks > m.arriveTicks)
|
||||
{
|
||||
// пока шли, поселение уже разграбили -- возвращаемся ни с чем
|
||||
for (int i = 0; i < 4; i++) m.survivors[i] = m.units[i];
|
||||
m.returning = true;
|
||||
m.returnTicks = m.arriveTicks + travel;
|
||||
AddReport(false, "Поход отменён: " + def.name, "Поселение уже разграблено. Армия возвращается домой без боя.");
|
||||
GP.MarkChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
int level = s.level;
|
||||
var defenders = (int[])s.garrison.Clone();
|
||||
var sim = Simulate(m.units, defenders, GP.ArmyBonus, WorldData.DefenderBonus, true);
|
||||
|
||||
for (int i = 0; i < 4; i++) m.survivors[i] = sim.attLeft[i];
|
||||
s.garrison = sim.defLeft;
|
||||
s.lastBattleTicks = m.arriveTicks;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Цель: " + def.name + " (ур. " + level + ")");
|
||||
sb.AppendLine("Раундов: " + sim.rounds);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Ваша армия: " + UnitsText(m.units));
|
||||
sb.AppendLine("Ваши потери: " + LossText(m.units, sim.attLeft));
|
||||
sb.AppendLine("Защитники: " + UnitsText(defenders));
|
||||
sb.AppendLine("Потери врага: " + LossText(defenders, sim.defLeft));
|
||||
sb.AppendLine();
|
||||
foreach (var line in sim.log) sb.AppendLine(line);
|
||||
|
||||
if (sim.win)
|
||||
{
|
||||
// добыча ограничена грузоподъёмностью выживших
|
||||
var loot = FullLoot(def, level);
|
||||
int total = 0; foreach (var r in loot) total += r.amount;
|
||||
int carry = Carry(sim.attLeft);
|
||||
float k = total > 0 ? Mathf.Min(1f, (float)carry / total) : 0f;
|
||||
m.loot.Clear();
|
||||
foreach (var r in loot)
|
||||
{
|
||||
int a = Mathf.FloorToInt(r.amount * k);
|
||||
if (a > 0) m.loot.Add(new RewardItem(r.type, a));
|
||||
}
|
||||
|
||||
s.timesRaided++;
|
||||
s.level++;
|
||||
s.garrison = new int[4];
|
||||
s.ruinedUntilTicks = m.arriveTicks + TimeSpan.FromMinutes(def.respawnMinutes).Ticks;
|
||||
D.worldWins++;
|
||||
GP.AddEventPoints(15);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Добыча: " + (m.loot.Count > 0 ? GameData.RewardsText(m.loot.ToArray()) : "нечего унести") +
|
||||
(k < 1f ? " (не хватило грузоподъёмности)" : ""));
|
||||
sb.AppendLine("Поселение отстроится через " + GameData.FormatTime(def.respawnMinutes * 60) + " и станет сильнее.");
|
||||
AddReport(true, "Победа: " + def.name, sb.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
D.worldLosses++;
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(Count(sim.attLeft) > 0 ? "Армия не смогла взять поселение и отступает." : "Армия разбита.");
|
||||
AddReport(false, "Поражение: " + def.name, sb.ToString());
|
||||
}
|
||||
|
||||
if (Count(m.survivors) == 0)
|
||||
{
|
||||
D.marches.Remove(m); // некому возвращаться
|
||||
UIKit.Toast("Армия погибла у «" + def.name + "»");
|
||||
}
|
||||
else
|
||||
{
|
||||
m.returning = true;
|
||||
m.returnTicks = m.arriveTicks + travel;
|
||||
UIKit.Message(sim.win ? "ПОБЕДА!" : "Поражение",
|
||||
def.name + "\nВаши потери: " + LossText(m.units, sim.attLeft) +
|
||||
(sim.win ? "\nДобыча: " + (m.loot.Count > 0 ? GameData.RewardsText(m.loot.ToArray()) : "-") : "") +
|
||||
"\n\nАрмия возвращается домой: " + GameData.FormatTime(SecondsLeft(m)) + "\nПолный отчёт -- в Почте.", "ОК");
|
||||
}
|
||||
GP.MarkChanged();
|
||||
}
|
||||
|
||||
static void ReturnHome(WorldMarch m)
|
||||
{
|
||||
for (int i = 0; i < 4; i++) D.troops[i] += m.survivors[i];
|
||||
D.marches.Remove(m);
|
||||
var def = WorldData.Get(m.targetId);
|
||||
if (m.loot.Count > 0)
|
||||
{
|
||||
int xp = 20 + 10 * State(m.targetId).timesRaided;
|
||||
GP.Grant(m.loot.ToArray(), xp, false);
|
||||
UIKit.Toast("Армия вернулась из похода (" + def.name + "). Добыча на складе: " + GameData.RewardsText(m.loot.ToArray()));
|
||||
}
|
||||
else
|
||||
{
|
||||
UIKit.Toast("Армия вернулась домой: " + Count(m.survivors) + " воинов");
|
||||
}
|
||||
GP.MarkChanged();
|
||||
}
|
||||
|
||||
static void AddReport(bool win, string title, string text)
|
||||
{
|
||||
D.reports.Insert(0, new BattleReport { ticks = DateTime.Now.Ticks, title = title, text = text, win = win });
|
||||
if (D.reports.Count > 15) D.reports.RemoveAt(D.reports.Count - 1);
|
||||
GP.SendMail("Отчёт о бою: " + title, text);
|
||||
}
|
||||
|
||||
// ================= БОЙ =================
|
||||
|
||||
public class SimResult
|
||||
{
|
||||
public bool win;
|
||||
public int rounds;
|
||||
public int[] attLeft = new int[4];
|
||||
public int[] defLeft = new int[4];
|
||||
public List<string> log = new List<string>();
|
||||
}
|
||||
|
||||
// Пошаговый бой: каждый раунд обе стороны одновременно наносят урон.
|
||||
// Урон стороны = сумма атак всех воинов * бонус * случайность (±15%).
|
||||
// Урон делится между типами врага пропорционально их суммарному здоровью,
|
||||
// защита снижает урон (каждая единица -8%). Погибшие = урон / здоровье.
|
||||
// Победа -- у врага никого не осталось. За 10 раундов не справились -- отступление (поражение).
|
||||
public static SimResult Simulate(int[] attackers, int[] defenders, float attBonus, float defBonus, bool withLog)
|
||||
{
|
||||
var r = new SimResult();
|
||||
float[] a = new float[4], d = new float[4];
|
||||
for (int i = 0; i < 4; i++) { a[i] = attackers[i]; d[i] = defenders[i]; }
|
||||
|
||||
for (int round = 1; round <= WorldData.MaxRounds; round++)
|
||||
{
|
||||
if (Sum(a) < 0.5f || Sum(d) < 0.5f) break;
|
||||
float dmgA = Damage(a, attBonus);
|
||||
float dmgD = Damage(d, defBonus);
|
||||
float killedD = Apply(d, dmgA);
|
||||
float killedA = Apply(a, dmgD);
|
||||
r.rounds = round;
|
||||
if (withLog)
|
||||
r.log.Add(string.Format("Раунд {0}: вы нанесли {1:0} урона (убито {2:0}), враг нанёс {3:0} (убито {4:0})",
|
||||
round, dmgA, killedD, dmgD, killedA));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
r.attLeft[i] = Mathf.Clamp(Mathf.RoundToInt(a[i]), 0, attackers[i]);
|
||||
r.defLeft[i] = Mathf.Clamp(Mathf.RoundToInt(d[i]), 0, defenders[i]);
|
||||
}
|
||||
r.win = Count(r.defLeft) == 0 && Count(r.attLeft) > 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
static float Sum(float[] x) { float s = 0; foreach (var v in x) s += v; return s; }
|
||||
|
||||
static float Damage(float[] side, float bonus)
|
||||
{
|
||||
float dmg = 0;
|
||||
for (int i = 0; i < 4; i++) dmg += side[i] * WorldData.Units[i].attack;
|
||||
return dmg * (1f + bonus) * UnityEngine.Random.Range(0.85f, 1.15f);
|
||||
}
|
||||
|
||||
static float Apply(float[] side, float dmg)
|
||||
{
|
||||
float totalHp = 0;
|
||||
for (int i = 0; i < 4; i++) totalHp += side[i] * WorldData.Units[i].hp;
|
||||
if (totalHp <= 0) return 0;
|
||||
float killed = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (side[i] <= 0) continue;
|
||||
var u = WorldData.Units[i];
|
||||
float share = side[i] * u.hp / totalHp;
|
||||
float k = share * dmg / (u.hp * (1f + u.defense * 0.08f));
|
||||
k = Mathf.Min(k, side[i]);
|
||||
side[i] -= k;
|
||||
killed += k;
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
|
||||
// Оценка шанса: прогоняем бой много раз и считаем долю побед
|
||||
public static float EstimateWinChance(int[] attackers, string targetId)
|
||||
{
|
||||
if (Count(attackers) == 0) return 0f;
|
||||
var s = State(targetId);
|
||||
int wins = 0;
|
||||
const int runs = 40;
|
||||
for (int k = 0; k < runs; k++)
|
||||
if (Simulate(attackers, s.garrison, GP.ArmyBonus, WorldData.DefenderBonus, false).win) wins++;
|
||||
return (float)wins / runs;
|
||||
}
|
||||
|
||||
// ================= ТЕКСТЫ =================
|
||||
|
||||
public static string UnitsText(int[] units)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int i = 0; i < 4; i++) if (units[i] > 0) parts.Add(GameData.Troops[i].name + " " + units[i]);
|
||||
return parts.Count == 0 ? "нет" : string.Join(", ", parts.ToArray());
|
||||
}
|
||||
|
||||
public static string LossText(int[] before, int[] after)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int lost = before[i] - after[i];
|
||||
if (lost > 0) parts.Add(GameData.Troops[i].name + " " + lost);
|
||||
}
|
||||
return parts.Count == 0 ? "нет" : string.Join(", ", parts.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb4b3ef256809b94f817e98df155b714
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
// Сохраняет и загружает: все ресурсы, уровень Ратуши, список всех построенных зданий
|
||||
// (какой тип, где стоит, какого уровня).
|
||||
@@ -51,6 +52,24 @@ public class SaveManager : MonoBehaviour
|
||||
}
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this) SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
// [объединение с картой Саши] Первый вход в город загружает GameBootstrap.
|
||||
// При возврате в город со сцены WorldMap GameBootstrap уничтожается вместе с дубликатом SaveManager,
|
||||
// поэтому здания восстанавливаем здесь (из сохранения, сделанного прямо перед уходом из города).
|
||||
bool cityLoadedOnce;
|
||||
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
if (BuildManager.Instance == null) return; // это не город
|
||||
// ресурсы НЕ перечитываем: они живут в памяти между сценами (добыча с карты мира не должна пропасть)
|
||||
if (cityLoadedOnce && HasSaveFile()) LoadGame(false);
|
||||
cityLoadedOnce = true;
|
||||
}
|
||||
|
||||
void Start()
|
||||
@@ -78,6 +97,10 @@ public class SaveManager : MonoBehaviour
|
||||
|
||||
public void SaveGame()
|
||||
{
|
||||
// На карте мира города нет -- сохранение затёрло бы все здания. Город сохраняется перед уходом.
|
||||
if (BuildManager.Instance == null) return;
|
||||
cityLoadedOnce = true;
|
||||
|
||||
SaveData data = new SaveData();
|
||||
|
||||
// Ресурсы
|
||||
@@ -125,8 +148,12 @@ public class SaveManager : MonoBehaviour
|
||||
return File.Exists(SavePath);
|
||||
}
|
||||
|
||||
public void LoadGame()
|
||||
public void LoadGame() { LoadGame(true); }
|
||||
|
||||
public void LoadGame(bool includeResources)
|
||||
{
|
||||
BarracksBootstrap.Ensure(); // казармы должны быть в списке стройки до восстановления зданий
|
||||
|
||||
if (!HasSaveFile())
|
||||
{
|
||||
Debug.Log("[ЗАГРУЗКА] Файл сохранения не найден, начинаем новую игру.");
|
||||
@@ -137,7 +164,7 @@ public class SaveManager : MonoBehaviour
|
||||
SaveData data = JsonUtility.FromJson<SaveData>(json);
|
||||
|
||||
// Ресурсы
|
||||
if (ResourceManager.Instance != null)
|
||||
if (includeResources && ResourceManager.Instance != null)
|
||||
{
|
||||
foreach (var entry in data.resources)
|
||||
{
|
||||
@@ -171,6 +198,7 @@ public class SaveManager : MonoBehaviour
|
||||
|
||||
Vector3 pos = new Vector3(entry.posX, entry.posY, 0f);
|
||||
GameObject built = Instantiate(matchingData.prefab, pos, Quaternion.identity);
|
||||
if (!built.activeSelf) built.SetActive(true); // шаблон казарм хранится выключенным
|
||||
|
||||
PlacedBuildingId idMarker = built.AddComponent<PlacedBuildingId>();
|
||||
idMarker.buildingName = entry.buildingName;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class UINavigation : MonoBehaviour
|
||||
{
|
||||
[Header("Родитель панелей (например, HUD_Canvas или PanelsContainer)")]
|
||||
[SerializeField] private Transform panelsRoot;
|
||||
|
||||
// Публичные методы для кнопок (вешаем в OnClick)
|
||||
public void OpenHome() => ShowPanel("Panel_Home");
|
||||
public void OpenHero() => ShowPanel("Panel_Hero");
|
||||
public void OpenArena() => ShowPanel("Panel_Arena");
|
||||
public void OpenBattle() => ShowPanel("Panel_Battle");
|
||||
public void OpenTroops() => ShowPanel("Panel_Troops");
|
||||
public void OpenShop() => ShowPanel("Panel_Shop");
|
||||
public void OpenMail() => ShowPanel("Panel_Mail");
|
||||
public void OpenMissions() => ShowPanel("Panel_Missions");
|
||||
public void OpenProfile() => ShowPanel("Panel_Profile");
|
||||
public void OpenReward() => ShowPanel("Panel_Reward");
|
||||
public void OpenEvents() => ShowPanel("Panel_Events");
|
||||
|
||||
private void ShowPanel(string panelName)
|
||||
{
|
||||
GameObject target = null; // Объявляем переменную ОДИН раз в начале метода
|
||||
|
||||
// Вариант 1: Если задан родитель, ищем только внутри него
|
||||
if (panelsRoot != null)
|
||||
{
|
||||
Transform found = panelsRoot.Find(panelName);
|
||||
if (found != null)
|
||||
target = found.gameObject;
|
||||
}
|
||||
// Вариант 2: Если родителя нет, ищем по всей сцене
|
||||
else
|
||||
{
|
||||
target = GameObject.Find(panelName);
|
||||
}
|
||||
|
||||
// Если панель найдена — показываем её
|
||||
if (target != null)
|
||||
{
|
||||
HideAllPanels();
|
||||
target.SetActive(true);
|
||||
Debug.Log("Открыта панель: " + panelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Не удалось найти панель с именем: " + panelName + ". Проверь название!");
|
||||
}
|
||||
}
|
||||
|
||||
private void HideAllPanels()
|
||||
{
|
||||
if (panelsRoot == null) return;
|
||||
|
||||
foreach (Transform child in panelsRoot)
|
||||
{
|
||||
child.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44d831ce8edf82d4393ffaae110a0ed2
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2bd5411b154aeb4d94a3df600743672
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Панель чата: вкладки "Общий мир" / "Круг", лента сообщений, поле ввода и кнопка отправки.
|
||||
public class ChatPanel : MonoBehaviour
|
||||
{
|
||||
[Header("Вкладки")]
|
||||
public Button worldTabButton;
|
||||
public Button circleTabButton;
|
||||
|
||||
[Header("Сообщения")]
|
||||
public TMP_Text messagesText;
|
||||
|
||||
[Header("Ввод")]
|
||||
public TMP_InputField inputField;
|
||||
public Button sendButton;
|
||||
|
||||
const int MaxShownMessages = 30; // старые всё равно уходят за верхний край ленты
|
||||
|
||||
static readonly Color ActiveTabColor = new Color(0.16f, 0.35f, 0.7f);
|
||||
static readonly Color InactiveTabColor = new Color(0.12f, 0.13f, 0.17f);
|
||||
static readonly string[] NameColors = { "#6EC6FF", "#FF8A65", "#C792EA", "#81C784", "#FFD54F", "#4DD0E1" };
|
||||
const string OwnNameColor = "#FFC857";
|
||||
|
||||
private ChatChannel currentChannel = ChatChannel.World;
|
||||
private readonly StringBuilder sb = new StringBuilder();
|
||||
|
||||
void Start()
|
||||
{
|
||||
worldTabButton.onClick.AddListener(() => SwitchChannel(ChatChannel.World));
|
||||
circleTabButton.onClick.AddListener(() => SwitchChannel(ChatChannel.Circle));
|
||||
sendButton.onClick.AddListener(Send);
|
||||
inputField.onSubmit.AddListener(_ => Send()); // Enter на клавиатуре
|
||||
inputField.characterLimit = ChatService.MaxMessageLength;
|
||||
|
||||
ChatService.Instance.MessageReceived += OnMessageReceived;
|
||||
SwitchChannel(ChatChannel.World);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
ChatService.Instance.MessageReceived -= OnMessageReceived;
|
||||
}
|
||||
|
||||
void SwitchChannel(ChatChannel channel)
|
||||
{
|
||||
currentChannel = channel;
|
||||
worldTabButton.image.color = channel == ChatChannel.World ? ActiveTabColor : InactiveTabColor;
|
||||
circleTabButton.image.color = channel == ChatChannel.Circle ? ActiveTabColor : InactiveTabColor;
|
||||
Redraw();
|
||||
}
|
||||
|
||||
void Send()
|
||||
{
|
||||
if (ChatService.Instance.Send(currentChannel, inputField.text))
|
||||
{
|
||||
inputField.text = "";
|
||||
}
|
||||
inputField.ActivateInputField(); // курсор остаётся в поле -- можно писать дальше
|
||||
}
|
||||
|
||||
void OnMessageReceived(ChatMessage message)
|
||||
{
|
||||
if (message.channel == currentChannel) Redraw();
|
||||
}
|
||||
|
||||
void Redraw()
|
||||
{
|
||||
IReadOnlyList<ChatMessage> messages = ChatService.Instance.GetMessages(currentChannel);
|
||||
int start = Mathf.Max(0, messages.Count - MaxShownMessages);
|
||||
|
||||
sb.Clear();
|
||||
for (int i = start; i < messages.Count; i++)
|
||||
{
|
||||
ChatMessage m = messages[i];
|
||||
string time = DateTimeOffset.FromUnixTimeMilliseconds(m.timeMs).ToLocalTime().ToString("HH:mm");
|
||||
string color = m.senderId == MarchManager.LocalPlayerId ? OwnNameColor : GetNameColor(m.senderId);
|
||||
|
||||
if (sb.Length > 0) sb.Append('\n');
|
||||
// <noparse> -- чтобы игрок не мог вставить в сообщение теги оформления
|
||||
sb.Append($"<color=#9AA3B5>[{time}]</color> <color={color}>{m.senderName}:</color> <noparse>{m.text}</noparse>");
|
||||
}
|
||||
messagesText.text = sb.ToString();
|
||||
}
|
||||
|
||||
// Один и тот же игрок -- всегда один цвет
|
||||
static string GetNameColor(string senderId)
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (char c in senderId) hash = hash * 31 + c;
|
||||
return NameColors[(hash & int.MaxValue) % NameColors.Length];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baf21465e5a2a3f448d21131289986a9
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public enum ChatChannel
|
||||
{
|
||||
World, // общий чат всего мира
|
||||
Circle // чат своего Круга
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ChatMessage
|
||||
{
|
||||
public ChatChannel channel;
|
||||
public string senderId;
|
||||
public string senderName;
|
||||
public string text;
|
||||
public long timeMs; // Unix-время в миллисекундах
|
||||
}
|
||||
|
||||
// История чата. Обычный класс со статическим экземпляром -- переживает смену сцен,
|
||||
// поэтому переписка не теряется при переходе город <-> карта. Пока всё локально;
|
||||
// когда появится сервер, Send будет отправлять сообщение туда, а входящие -- приходить через MessageReceived.
|
||||
public class ChatService
|
||||
{
|
||||
public const int MaxMessageLength = 200;
|
||||
public const int MaxStoredPerChannel = 100;
|
||||
|
||||
// Пока аккаунтов нет -- одно имя для локального игрока
|
||||
public const string LocalPlayerName = "Богатырь";
|
||||
|
||||
private static ChatService instance;
|
||||
public static ChatService Instance => instance ??= new ChatService(MarchManager.NowMs);
|
||||
|
||||
public event Action<ChatMessage> MessageReceived;
|
||||
|
||||
private readonly Dictionary<ChatChannel, List<ChatMessage>> history = new Dictionary<ChatChannel, List<ChatMessage>>
|
||||
{
|
||||
{ ChatChannel.World, new List<ChatMessage>() },
|
||||
{ ChatChannel.Circle, new List<ChatMessage>() }
|
||||
};
|
||||
|
||||
ChatService(long nowMs)
|
||||
{
|
||||
SeedDemoMessages(nowMs);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ChatMessage> GetMessages(ChatChannel channel)
|
||||
{
|
||||
return history[channel];
|
||||
}
|
||||
|
||||
public bool Send(ChatChannel channel, string text)
|
||||
{
|
||||
text = text?.Trim();
|
||||
if (string.IsNullOrEmpty(text)) return false;
|
||||
if (text.Length > MaxMessageLength) text = text.Substring(0, MaxMessageLength);
|
||||
|
||||
Add(new ChatMessage
|
||||
{
|
||||
channel = channel,
|
||||
senderId = MarchManager.LocalPlayerId,
|
||||
senderName = LocalPlayerName,
|
||||
text = text,
|
||||
timeMs = MarchManager.NowMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void Add(ChatMessage message)
|
||||
{
|
||||
List<ChatMessage> list = history[message.channel];
|
||||
list.Add(message);
|
||||
if (list.Count > MaxStoredPerChannel) list.RemoveAt(0);
|
||||
MessageReceived?.Invoke(message);
|
||||
}
|
||||
|
||||
// Несколько сообщений "от других игроков", чтобы чат не был пустым. Уберётся с приходом сервера.
|
||||
void SeedDemoMessages(long nowMs)
|
||||
{
|
||||
const long minute = 60_000;
|
||||
AddDemo(ChatChannel.World, "npc_sever", "Север", "Кто идёт на Змея Горыныча?", nowMs - 5 * minute);
|
||||
AddDemo(ChatChannel.World, "npc_dreadwolf", "DreadWolf", "Набор в Круг, нужны активные игроки!", nowMs - 4 * minute);
|
||||
AddDemo(ChatChannel.World, "npc_moroz", "Мороз", "На западе замечен Круг Стальные Волки.", nowMs - 3 * minute);
|
||||
AddDemo(ChatChannel.World, "npc_tairon", "Тайрон", "Собираем отряд на древний храм.", nowMs - 2 * minute);
|
||||
AddDemo(ChatChannel.World, "npc_veles", "Велес", "Всем удачной охоты!", nowMs - minute);
|
||||
|
||||
AddDemo(ChatChannel.Circle, "npc_ratibor", "Ратибор", "Братья, завтра вместе идём на замок.", nowMs - 10 * minute);
|
||||
AddDemo(ChatChannel.Circle, "npc_svetozar", "Светозар", "Скиньте еды в казну Круга, кому не жалко.", nowMs - 6 * minute);
|
||||
}
|
||||
|
||||
void AddDemo(ChatChannel channel, string id, string name, string text, long timeMs)
|
||||
{
|
||||
history[channel].Add(new ChatMessage { channel = channel, senderId = id, senderName = name, text = text, timeMs = timeMs });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee8544a4534237643ada27a572040574
|
||||
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Одна строка таблицы рейтинга (ссылки на её элементы; заполняет RatingPanel)
|
||||
[Serializable]
|
||||
public class RatingRowView
|
||||
{
|
||||
public GameObject root;
|
||||
public Image background;
|
||||
public Image medal;
|
||||
public TMP_Text placeText;
|
||||
public Image avatar;
|
||||
public TMP_Text avatarLetter;
|
||||
public TMP_Text nameText;
|
||||
public Image circleBanner;
|
||||
public TMP_Text circleText;
|
||||
public TMP_Text powerText;
|
||||
public Button inspectButton;
|
||||
}
|
||||
|
||||
// Панель "Рейтинг": разделы слева, таблица "Место / Игрок / Круг / Мощь", страницы внизу.
|
||||
public class RatingPanel : MonoBehaviour
|
||||
{
|
||||
public const int PageSize = 10;
|
||||
|
||||
[Header("Общее")]
|
||||
public Button closeButton;
|
||||
public TMP_Text statusText;
|
||||
|
||||
[Header("Разделы (порядок как в RatingCategory)")]
|
||||
public Button[] categoryButtons;
|
||||
|
||||
[Header("Таблица")]
|
||||
public TMP_Text nameColumnHeader;
|
||||
public TMP_Text circleColumnHeader;
|
||||
public RatingRowView[] rows;
|
||||
public TMP_Text emptyText;
|
||||
|
||||
[Header("Страницы")]
|
||||
public Button prevPageButton;
|
||||
public Button nextPageButton;
|
||||
public Button[] pageButtons;
|
||||
|
||||
static readonly Color GoldRow = new Color(0.45f, 0.32f, 0.1f, 0.95f);
|
||||
static readonly Color SilverRow = new Color(0.28f, 0.31f, 0.36f, 0.95f);
|
||||
static readonly Color BronzeRow = new Color(0.36f, 0.22f, 0.12f, 0.95f);
|
||||
static readonly Color EvenRow = new Color(0.07f, 0.09f, 0.13f, 0.95f);
|
||||
static readonly Color OddRow = new Color(0.1f, 0.12f, 0.17f, 0.95f);
|
||||
static readonly Color[] MedalColors = { new Color(0.95f, 0.75f, 0.3f), new Color(0.8f, 0.83f, 0.88f), new Color(0.8f, 0.5f, 0.28f) };
|
||||
|
||||
static readonly Color ActiveButton = new Color(0.16f, 0.35f, 0.7f);
|
||||
static readonly Color InactiveButton = new Color(0.1f, 0.12f, 0.17f);
|
||||
|
||||
static readonly Color[] BannerColors =
|
||||
{
|
||||
new Color(0.15f, 0.35f, 0.75f), new Color(0.45f, 0.2f, 0.18f), new Color(0.7f, 0.15f, 0.15f),
|
||||
new Color(0.2f, 0.45f, 0.2f), new Color(0.75f, 0.5f, 0.1f), new Color(0.35f, 0.35f, 0.4f)
|
||||
};
|
||||
|
||||
private RatingCategory category = RatingCategory.Players;
|
||||
private int pageIndex;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
closeButton.onClick.AddListener(Close);
|
||||
prevPageButton.onClick.AddListener(() => ShowPage(pageIndex - 1));
|
||||
nextPageButton.onClick.AddListener(() => ShowPage(pageIndex + 1));
|
||||
|
||||
for (int i = 0; i < categoryButtons.Length; i++)
|
||||
{
|
||||
RatingCategory c = (RatingCategory)i;
|
||||
categoryButtons[i].onClick.AddListener(() => SelectCategory(c));
|
||||
}
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Круглая медаль -- спрайт генерируется в коде, поэтому ставим его здесь, а не в сборщике сцены
|
||||
row.medal.sprite = PrimitiveSprites.Circle;
|
||||
}
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
SelectCategory(category);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void SelectCategory(RatingCategory newCategory)
|
||||
{
|
||||
category = newCategory;
|
||||
for (int i = 0; i < categoryButtons.Length; i++)
|
||||
categoryButtons[i].image.color = i == (int)category ? ActiveButton : InactiveButton;
|
||||
|
||||
bool circles = category == RatingCategory.Circles;
|
||||
nameColumnHeader.text = circles ? "Круг" : "Игрок";
|
||||
circleColumnHeader.text = circles ? "Глава" : "Круг";
|
||||
|
||||
ShowPage(0);
|
||||
}
|
||||
|
||||
int PageCount => Mathf.Max(1, Mathf.CeilToInt(RatingService.Instance.GetTotalCount(category) / (float)PageSize));
|
||||
|
||||
void ShowPage(int newPageIndex)
|
||||
{
|
||||
pageIndex = Mathf.Clamp(newPageIndex, 0, PageCount - 1);
|
||||
statusText.text = "";
|
||||
|
||||
bool available = RatingService.Instance.IsAvailable(category);
|
||||
emptyText.gameObject.SetActive(!available);
|
||||
emptyText.text = "Раздел в разработке";
|
||||
|
||||
List<RatingEntry> entries = available
|
||||
? RatingService.Instance.GetPage(category, pageIndex, PageSize)
|
||||
: new List<RatingEntry>();
|
||||
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
if (i < entries.Count) FillRow(rows[i], entries[i], i);
|
||||
else rows[i].root.SetActive(false);
|
||||
}
|
||||
|
||||
UpdatePagination(available);
|
||||
}
|
||||
|
||||
void FillRow(RatingRowView row, RatingEntry entry, int indexOnPage)
|
||||
{
|
||||
row.root.SetActive(true);
|
||||
|
||||
bool top3 = entry.place <= 3;
|
||||
row.background.color = entry.place == 1 ? GoldRow
|
||||
: entry.place == 2 ? SilverRow
|
||||
: entry.place == 3 ? BronzeRow
|
||||
: indexOnPage % 2 == 0 ? EvenRow : OddRow;
|
||||
|
||||
row.medal.gameObject.SetActive(top3);
|
||||
if (top3) row.medal.color = MedalColors[entry.place - 1];
|
||||
row.placeText.text = entry.place.ToString();
|
||||
row.placeText.color = top3 ? new Color(0.2f, 0.12f, 0.05f) : Color.white;
|
||||
|
||||
// Аватар-заглушка: цветной квадрат с первой буквой имени
|
||||
row.avatar.color = Color.Lerp(BannerColors[StableHash(entry.name) % BannerColors.Length], Color.black, 0.25f);
|
||||
row.avatarLetter.text = entry.name.Substring(0, 1).ToUpper();
|
||||
|
||||
row.nameText.text = entry.name;
|
||||
row.circleText.text = entry.circleName;
|
||||
row.circleBanner.color = BannerColors[StableHash(category == RatingCategory.Circles ? entry.name : entry.circleName) % BannerColors.Length];
|
||||
row.powerText.text = FormatPower(entry.power);
|
||||
|
||||
string name = entry.name;
|
||||
row.inspectButton.onClick.RemoveAllListeners();
|
||||
row.inspectButton.onClick.AddListener(() => statusText.text = $"Профиль «{name}» пока недоступен");
|
||||
}
|
||||
|
||||
void UpdatePagination(bool available)
|
||||
{
|
||||
int pageCount = PageCount;
|
||||
prevPageButton.interactable = available && pageIndex > 0;
|
||||
nextPageButton.interactable = available && pageIndex < pageCount - 1;
|
||||
|
||||
// Окно из 5 номеров вокруг текущей страницы
|
||||
int first = Mathf.Clamp(pageIndex - pageButtons.Length / 2, 0, Mathf.Max(0, pageCount - pageButtons.Length));
|
||||
for (int i = 0; i < pageButtons.Length; i++)
|
||||
{
|
||||
int page = first + i;
|
||||
Button button = pageButtons[i];
|
||||
bool visible = available && page < pageCount;
|
||||
button.gameObject.SetActive(visible);
|
||||
if (!visible) continue;
|
||||
|
||||
button.GetComponentInChildren<TMP_Text>().text = (page + 1).ToString();
|
||||
button.image.color = page == pageIndex ? ActiveButton : InactiveButton;
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => ShowPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
// 128530000 -> "128 530 000"
|
||||
static string FormatPower(long power)
|
||||
{
|
||||
return power.ToString("N0", CultureInfo.InvariantCulture).Replace(',', ' ');
|
||||
}
|
||||
|
||||
// string.GetHashCode в Unity может меняться между запусками -- для цвета баннера нужен стабильный
|
||||
static int StableHash(string s)
|
||||
{
|
||||
int hash = 17;
|
||||
foreach (char c in s) hash = hash * 31 + c;
|
||||
return hash & int.MaxValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c9bb8e0e4ad65a4d886aa354a85da52
|
||||
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// Разделы рейтинга (как вкладки слева на панели)
|
||||
public enum RatingCategory
|
||||
{
|
||||
Players,
|
||||
Circles,
|
||||
Power,
|
||||
PvP,
|
||||
Development,
|
||||
Season
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RatingEntry
|
||||
{
|
||||
public int place;
|
||||
public string id; // id игрока или Круга
|
||||
public string name; // имя игрока или название Круга
|
||||
public string circleName; // Круг игрока (для рейтинга Кругов -- имя главы)
|
||||
public long power;
|
||||
}
|
||||
|
||||
// Рейтинг. Пока заглушка: 50 выдуманных игроков, числа -- как на референсе, для проверки вёрстки.
|
||||
// Когда появится сервер, он будет считать рейтинг и отдавать его постранично через GetPage.
|
||||
public class RatingService
|
||||
{
|
||||
private static RatingService instance;
|
||||
public static RatingService Instance => instance ??= new RatingService();
|
||||
|
||||
private readonly List<RatingEntry> players = new List<RatingEntry>();
|
||||
private readonly List<RatingEntry> circles = new List<RatingEntry>();
|
||||
|
||||
static readonly string[] PlayerNames =
|
||||
{
|
||||
"Велес", "Север", "Тайрон", "DreadWolf", "Мороз", "Леший", "Князь", "Варяг", "Сокол", "Ратибор",
|
||||
"Светозар", "Добромир", "Велимир", "Ярополк", "Всеслав", "Мстислав", "Остромир", "Богдан", "Любомир", "Твердислав",
|
||||
"Радим", "Гостомысл", "Вышеслав", "Путята", "Ставр", "Чурила", "Микула", "Василиса", "Забава", "IronBear",
|
||||
"Ярослав", "Горазд", "Будимир", "Ждан", "Первуша", "Томир", "Вадим", "Олег", "Игорь", "Святослав",
|
||||
"Рогволд", "Судислав", "Любава", "Милава", "Велеслава", "Snowfox", "Berserk", "Кузьма", "Потап", "Никита"
|
||||
};
|
||||
|
||||
static readonly string[] CircleNames =
|
||||
{
|
||||
"Круг Перуна", "Стальные Волки", "Сыны Руси", "Богатыри", "Северный Страж",
|
||||
"Вольные", "Златые Земли", "Серые Горны", "Речные", "Перунов Круг"
|
||||
};
|
||||
|
||||
RatingService()
|
||||
{
|
||||
var rng = new Random(20260926);
|
||||
long power = 128_530_000;
|
||||
|
||||
for (int i = 0; i < PlayerNames.Length; i++)
|
||||
{
|
||||
players.Add(new RatingEntry
|
||||
{
|
||||
place = i + 1,
|
||||
id = $"player_{i + 1}",
|
||||
name = PlayerNames[i],
|
||||
circleName = CircleNames[i % CircleNames.Length],
|
||||
power = power
|
||||
});
|
||||
// Каждый следующий слабее на 5-15%
|
||||
power = power * rng.Next(85, 96) / 100;
|
||||
}
|
||||
|
||||
BuildCircleRating();
|
||||
}
|
||||
|
||||
void BuildCircleRating()
|
||||
{
|
||||
var totals = new Dictionary<string, long>();
|
||||
var leaders = new Dictionary<string, string>();
|
||||
foreach (var p in players)
|
||||
{
|
||||
totals.TryGetValue(p.circleName, out long sum);
|
||||
totals[p.circleName] = sum + p.power;
|
||||
if (!leaders.ContainsKey(p.circleName)) leaders[p.circleName] = p.name; // игроки уже по убыванию мощи
|
||||
}
|
||||
|
||||
foreach (var name in CircleNames)
|
||||
{
|
||||
circles.Add(new RatingEntry { id = name, name = name, circleName = leaders[name], power = totals[name] });
|
||||
}
|
||||
circles.Sort((a, b) => b.power.CompareTo(a.power));
|
||||
for (int i = 0; i < circles.Count; i++) circles[i].place = i + 1;
|
||||
}
|
||||
|
||||
List<RatingEntry> GetList(RatingCategory category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case RatingCategory.Players:
|
||||
case RatingCategory.Power: // пока мощь = рейтинг игроков
|
||||
return players;
|
||||
case RatingCategory.Circles:
|
||||
return circles;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAvailable(RatingCategory category)
|
||||
{
|
||||
return GetList(category) != null;
|
||||
}
|
||||
|
||||
public int GetTotalCount(RatingCategory category)
|
||||
{
|
||||
return GetList(category)?.Count ?? 0;
|
||||
}
|
||||
|
||||
public List<RatingEntry> GetPage(RatingCategory category, int pageIndex, int pageSize)
|
||||
{
|
||||
var result = new List<RatingEntry>();
|
||||
List<RatingEntry> list = GetList(category);
|
||||
if (list == null) return result;
|
||||
|
||||
int start = pageIndex * pageSize;
|
||||
for (int i = start; i < start + pageSize && i < list.Count; i++) result.Add(list[i]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user