2026-10-12 23:54:03 +03:00
|
|
|
|
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;
|
2026-10-23 18:33:04 +02:00
|
|
|
|
public static bool HasInstance { get { return instance != null; } }
|
2026-10-12 23:54:03 +03:00
|
|
|
|
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>();
|
2026-10-19 19:27:34 +02:00
|
|
|
|
ReturnLegacyWorldMarches();
|
2026-10-12 23:54:03 +03:00
|
|
|
|
if (Data.sashaMarches == null) Data.sashaMarches = new List<Bogatyri.World.MarchData>();
|
2026-10-23 18:33:04 +02:00
|
|
|
|
if (Data.sashaMarchUnits == null) Data.sashaMarchUnits = new List<LegacyMarchUnits>();
|
2026-10-12 23:54:03 +03:00
|
|
|
|
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); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 19:27:34 +02:00
|
|
|
|
// Старая маленькая карта мира удалена: солдаты из её походов возвращаются в город.
|
|
|
|
|
|
// Ещё идут к цели -- возвращаются все отправленные; уже идут обратно -- выжившие. Добыча таких походов не переносится.
|
|
|
|
|
|
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}.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// СБРОС СОХРАНЕНИЙ: прогресс (progress.json) + все модули (save.json): ресурсы, здания, Ратуша, походы.
|
|
|
|
|
|
// Кнопка в Настройках и меню Tools -> Сбросить сохранения. Документ: Docs/Modules/Save.md
|
2026-10-12 23:54:03 +03:00
|
|
|
|
public void ResetAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (File.Exists(SavePath)) File.Delete(SavePath);
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// Модули сбрасывают себя сами через модуль Сохранений; возврат ресурсов к старту -- не "сбор"
|
|
|
|
|
|
suppressTracking++;
|
|
|
|
|
|
if (GameServices.TryGet(out ISaveSystem save))
|
2026-10-12 23:54:03 +03:00
|
|
|
|
{
|
2026-10-19 18:39:28 +02:00
|
|
|
|
save.ResetAll();
|
2026-10-12 23:54:03 +03:00
|
|
|
|
}
|
2026-10-19 18:39:28 +02:00
|
|
|
|
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--;
|
2026-10-12 23:54:03 +03:00
|
|
|
|
|
|
|
|
|
|
Data = new ProgressData();
|
|
|
|
|
|
FixData();
|
2026-10-23 18:33:04 +02:00
|
|
|
|
Data.armyInModule = true; // войска сбросил модуль Армии, переносить из progress.json нечего
|
2026-10-12 23:54:03 +03:00
|
|
|
|
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;
|
2026-10-23 18:33:04 +02:00
|
|
|
|
for (int i = 0; i < GameData.Troops.Count; i++) p += TroopCount(i) * GameData.Troops[i].power;
|
2026-10-12 23:54:03 +03:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// ВОЙСКА
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
2026-10-23 18:33:04 +02:00
|
|
|
|
// Войска хранит модуль Армии (Docs/Modules/Army.md) -- здесь только обращение к нему
|
|
|
|
|
|
static IArmy Army => GameServices.Get<IArmy>();
|
|
|
|
|
|
|
|
|
|
|
|
public int TroopCount(int type) { return Army.GetTroops(type); }
|
|
|
|
|
|
|
2026-10-12 23:54:03 +03:00
|
|
|
|
public int TotalTroops
|
|
|
|
|
|
{
|
2026-10-23 18:33:04 +02:00
|
|
|
|
get { return Army.SoldiersAtHome; }
|
2026-10-12 23:54:03 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
Army.AddTroops(o.troopIndex, o.count);
|
2026-10-12 23:54:03 +03:00
|
|
|
|
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;
|
2026-10-23 18:33:04 +02:00
|
|
|
|
return Army.LoseFraction(fraction);
|
2026-10-12 23:54:03 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// БОЙ (кампания)
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|