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 bool HasInstance { get { return instance != null; } } public static GameProgress Instance { get { if (instance == null) { instance = FindAnyObjectByType(); if (instance == null) instance = new GameObject("GameProgress").AddComponent(); } 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(); 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(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(); if (Data.claimedDaily == null) Data.claimedDaily = new List(); if (Data.mail == null) Data.mail = new List(); if (Data.shopIds == null) Data.shopIds = new List(); if (Data.shopCounts == null) Data.shopCounts = new List(); if (Data.training == null) Data.training = new List(); if (Data.arenaOpponents == null) Data.arenaOpponents = new List(); if (Data.eventClaimed == null) Data.eventClaimed = new List(); ReturnLegacyWorldMarches(); if (Data.sashaMarches == null) Data.sashaMarches = new List(); if (Data.sashaMarchUnits == null) Data.sashaMarchUnits = new List(); 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); } } // Старая маленькая карта мира удалена: солдаты из её походов возвращаются в город. // Ещё идут к цели -- возвращаются все отправленные; уже идут обратно -- выжившие. Добыча таких походов не переносится. void ReturnLegacyWorldMarches() { if (Data.marches == null || Data.marches.Count == 0) { Data.marches = new List(); return; } int returned = 0; foreach (var m in Data.marches) { int[] back = m.returning ? m.survivors : m.units; if (back == null) continue; for (int i = 0; i < back.Length && i < Data.troops.Length; i++) { Data.troops[i] += Mathf.Max(0, back[i]); returned += Mathf.Max(0, back[i]); } } Data.marches.Clear(); dirty = true; SaveSoon(); Debug.Log($"[ПРОГРЕСС] Старая карта мира удалена: в город вернулось воинов из её походов: {returned}."); } // СБРОС СОХРАНЕНИЙ: прогресс (progress.json) + все модули (save.json): ресурсы, здания, Ратуша, походы. // Кнопка в Настройках и меню Tools -> Сбросить сохранения. Документ: Docs/Modules/Save.md public void ResetAll() { if (File.Exists(SavePath)) File.Delete(SavePath); // Модули сбрасывают себя сами через модуль Сохранений; возврат ресурсов к старту -- не "сбор" suppressTracking++; if (GameServices.TryGet(out ISaveSystem save)) { save.ResetAll(); } else { // Сцена без SaveManager (например, карту мира запустили напрямую из редактора) string modulesSave = Path.Combine(Application.persistentDataPath, "save.json"); if (File.Exists(modulesSave)) File.Delete(modulesSave); foreach (var participant in SaveRegistry.All) participant.ResetState(); } suppressTracking--; Data = new ProgressData(); FixData(); Data.armyInModule = true; // войска сбросил модуль Армии, переносить из progress.json нечего 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(); 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(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 += TroopCount(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(); 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(); } // ===================================================================== // ВОЙСКА // ===================================================================== // Войска хранит модуль Армии (Docs/Modules/Army.md) -- здесь только обращение к нему static IArmy Army => GameServices.Get(); public int TroopCount(int type) { return Army.GetTroops(type); } public int TotalTroops { get { return Army.SoldiersAtHome; } } 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); Army.AddTroops(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; return Army.LoseFraction(fraction); } // ===================================================================== // БОЙ (кампания) // ===================================================================== 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(); 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 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 Leaderboard() { var list = new List(); 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; } }