Files

948 lines
37 KiB
C#
Raw Permalink Normal View History

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<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();
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>();
ReturnLegacyWorldMarches();
if (Data.sashaMarches == null) Data.sashaMarches = new List<Bogatyri.World.MarchData>();
if (Data.sashaMarchUnits == null) Data.sashaMarchUnits = new List<LegacyMarchUnits>();
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<LegacyWorldMarch>(); 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<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 += 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<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();
}
// =====================================================================
// ВОЙСКА
// =====================================================================
// Войска хранит модуль Армии (Docs/Modules/Army.md) -- здесь только обращение к нему
static IArmy Army => GameServices.Get<IArmy>();
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<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;
}
}