using System; using System.Collections.Generic; using UnityEngine; using RR.Common.Prizes; using RR.Common.Settings; using RR.Common.UI; using RR.Common.Utility; using RR.Game2D.Core.Managers; using RR.Common.Controls; namespace RR.Common.Managers { /// /// Центральный менеджер системы призов. /// Не знает конкретики о призах — читает конфигурацию из JSON () /// и делегирует выдачу зарегистрированным исполнителям (). /// public class PrizeSystemManager : MonoBehaviour { public static PrizeSystemManager Instance { get; private set; } [Header("QR-коды (глобальная конфигурация)")] [SerializeField] private List qrCodes = new(); [SerializeField] private string phoneNumberMask = "+7XXXXXXXXXX"; /// Хранилище призов за уровни (JSON). public PrizeDatabase Database { get; private set; } /// /// Вызывается после выдачи любого приза. Параметры: тип приза, успех. /// public event Action OnAnyPrizeResult; // ── Исполнители ── private readonly Dictionary _executors = new(); // ── QR-коды ── private readonly List _generatedQRCodes = new(); private readonly Dictionary _qrCodesCacheByTier = new(); private List _defaultQRCodeConfigs = new(); private string _phoneNumber = string.Empty; // ────────────── Инициализация ────────────── public void Initialize() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); _defaultQRCodeConfigs = qrCodes != null ? new List(qrCodes) : new List(); // Загружаем базу призов из JSON Database = new PrizeDatabase(); Database.Load(); LoadQRCodeConfigs(); // QR-коды (глобальные) RefreshPhoneNumber(); GenerateQRCodes(); SubscribeToSettings(); // Регистрируем исполнителей — модальные окна регистрируют себя сами через static Current RegisterExecutor(new NonePrize()); RegisterExecutor(new QRCodePrize(() => QRCodeModal.Current, GetQRCodes)); RegisterExecutor(new HopperPrize()); RegisterExecutor(new PrizeStandPrize(() => PrizeStandModal.Current)); Debug.Log("[PrizeSystemManager] Инициализирован. " + $"Исполнители: {_executors.Count}, Уровней в базе: {Database.GetAll().Count}"); } private void OnDestroy() { UnsubscribeFromSettings(); if (Instance == this) Instance = null; } // ────────────── Публичный API ────────────── /// /// Выдаёт приз за текущий уровень (World-Stage). /// Конфигурация берётся из . /// public void GivePrizeForCurrentLevel() { GivePrizeForLevel(GetCurrentLevelName()); } /// /// Возвращает конфигурацию приза для текущего уровня. /// Если база недоступна или конфиг не найден, возвращает конфиг без приза. /// public LevelPrizeConfig GetPrizeConfigForCurrentLevel() { return GetPrizeConfigForLevel(GetCurrentLevelName()); } /// /// Возвращает конфигурацию приза для указанного уровня. /// Если база недоступна или конфиг не найден, возвращает конфиг без приза. /// public LevelPrizeConfig GetPrizeConfigForLevel(string levelName) { if (Database == null || string.IsNullOrWhiteSpace(levelName)) return new LevelPrizeConfig(); return Database.GetConfig(levelName); } /// /// Выдаёт приз за указанный уровень. /// public void GivePrizeForLevel(string levelName) { LevelPrizeConfig config = GetPrizeConfigForLevel(levelName); if (config.type == PrizeType.None) { Debug.Log($"[PrizeSystemManager] Уровень «{levelName}»: приз не назначен."); OnAnyPrizeResult?.Invoke(PrizeType.None, true); return; } if (!_executors.TryGetValue(config.type, out PrizeBase executor)) { Debug.LogWarning($"[PrizeSystemManager] Нет исполнителя для типа «{config.type}»."); return; } if (executor.IsGiving) { Debug.LogWarning($"[PrizeSystemManager] Исполнитель «{config.type}» занят."); return; } Debug.Log($"[PrizeSystemManager] Уровень «{levelName}» → приз «{config.type}»."); executor.Execute(this, config); } /// /// Обратная совместимость: выдаёт приз хоппером (если включён в настройках). /// public void GivePrize() { bool hopperEnabled = SettingsManager.Instance != null && SettingsManager.Instance.GetBool("prizes.giveSmallPrize"); if (hopperEnabled && _executors.TryGetValue(PrizeType.Hopper, out PrizeBase executor)) executor.Execute(this, new LevelPrizeConfig { type = PrizeType.Hopper }); else Debug.Log("[PrizeSystemManager] Хоппер отключён в настройках."); } /// /// Выдаёт приз указанного типа с конфигурацией по умолчанию. /// public void GivePrize(PrizeType type) { if (!_executors.TryGetValue(type, out PrizeBase executor)) { Debug.LogWarning($"[PrizeSystemManager] Нет исполнителя для «{type}»."); return; } executor.Execute(this, new LevelPrizeConfig { type = type }); } /// Возвращает QR-коды выбранного ранга. public Texture2D[] GetQRCodes(QRTier tier) { if (_qrCodesCacheByTier.TryGetValue(tier, out Texture2D[] cached)) { return cached; } List textures = new(); foreach (GeneratedQRCodeEntry entry in _generatedQRCodes) { if (entry.Texture == null) continue; if (tier == QRTier.None || entry.Tier == tier) textures.Add(entry.Texture); } if (textures.Count == 0 && tier != QRTier.None) { foreach (GeneratedQRCodeEntry entry in _generatedQRCodes) { if (entry.Texture != null && entry.Tier == QRTier.None) textures.Add(entry.Texture); } } Texture2D[] resolved = textures.ToArray(); _qrCodesCacheByTier[tier] = resolved; return resolved; } public List GetQRCodeConfigs() { return qrCodes != null ? new List(qrCodes) : new List(); } public void SetQRCodeConfigs(List configs) { qrCodes = MergeQRCodeConfigs(configs, _defaultQRCodeConfigs); Database?.SetQRCodes(qrCodes); GenerateQRCodes(); } /// Возвращает конфигурацию глобальной витрины. public PrizeStandSettings GetPrizeStandSettings() { return Database?.GetPrizeStandSettings() ?? new PrizeStandSettings(); } /// Сохраняет конфигурацию глобальной витрины. public void SetPrizeStandSettings(PrizeStandSettings settings) { Database?.SetPrizeStandSettings(settings); } public PrizeStandConnectionInfo GetPrizeStandConnectionInfo() { PrizeStandController controller = ComSystemManager.GetPrizeStandController(); if (controller == null) return new PrizeStandConnectionInfo(false, 0); return new PrizeStandConnectionInfo(true, controller.GetBoxesCount()); } /// /// Возвращает первый непустой короткий текст QR-приза по указанному приоритету рангов. /// По умолчанию ищет в порядке Gold → Silver → Bronze. /// public string GetPreferredQRCodePrizeText(params QRTier[] tierPriority) { QRTier[] tiers = tierPriority != null && tierPriority.Length > 0 ? tierPriority : new[] { QRTier.Gold, QRTier.Silver, QRTier.Bronze }; for (int tierIndex = 0; tierIndex < tiers.Length; tierIndex++) { QRTier tier = tiers[tierIndex]; for (int i = 0; i < qrCodes.Count; i++) { if (qrCodes[i].tier != tier) continue; string prizeText = qrCodes[i].prizeText; if (!string.IsNullOrWhiteSpace(prizeText)) return prizeText.Trim(); } } return string.Empty; } public string GetQRCodeModalPrizeText(QRTier tier) { string directMatch = GetQRCodeModalPrizeTextForTier(tier); if (!string.IsNullOrWhiteSpace(directMatch)) { return directMatch; } if (tier != QRTier.None) { string fallback = GetQRCodeModalPrizeTextForTier(QRTier.None); if (!string.IsNullOrWhiteSpace(fallback)) { return fallback; } } return string.Empty; } /// Идёт ли выдача какого-либо приза. public bool IsAnyPrizeGiving { get { foreach (PrizeBase ex in _executors.Values) if (ex.IsGiving) return true; return false; } } // ────────────── Регистрация исполнителей ────────────── /// Регистрирует исполнитель приза. Заменяет предыдущий для того же типа. public void RegisterExecutor(PrizeBase executor) { _executors[executor.Type] = executor; executor.OnPrizeResult += success => OnAnyPrizeResult?.Invoke(executor.Type, success); } // ────────────── QR-коды (глобальная генерация) ────────────── private void RefreshPhoneNumber() { var settings = SettingsManager.Instance; _phoneNumber = settings?.GetString("prizes.phoneNumber") ?? string.Empty; _phoneNumber = PhoneNumberUtils.ValidatePhoneNumber(_phoneNumber); _phoneNumber = PhoneNumberUtils.ApplyPhoneMask(_phoneNumber, phoneNumberMask); } private void LoadQRCodeConfigs() { if (Database == null) { return; } if (Database.HasStoredQRCodes) { List storedConfigs = Database.GetQRCodes(); List mergedConfigs = MergeQRCodeConfigs(storedConfigs, _defaultQRCodeConfigs); qrCodes = mergedConfigs; if (AreQRCodeConfigsDifferent(storedConfigs, mergedConfigs)) { Database.SetQRCodes(mergedConfigs); } return; } if (qrCodes != null && qrCodes.Count > 0) { Database.SetQRCodes(qrCodes); } } private string GetQRCodeModalPrizeTextForTier(QRTier tier) { for (int i = 0; i < qrCodes.Count; i++) { if (qrCodes[i].tier != tier) continue; string modalPrizeText = qrCodes[i].prizeText; if (!string.IsNullOrWhiteSpace(modalPrizeText)) { return modalPrizeText.Trim(); } } return string.Empty; } private static List MergeQRCodeConfigs(List storedConfigs, List defaultConfigs) { List merged = new(); List stored = storedConfigs ?? new List(); List defaults = defaultConfigs ?? new List(); if (stored.Count == 0) { return new List(defaults); } Dictionary tierUsage = new(); for (int i = 0; i < stored.Count; i++) { QRCodeInfo info = stored[i]; int sameTierIndex = tierUsage.TryGetValue(info.tier, out int currentIndex) ? currentIndex : 0; tierUsage[info.tier] = sameTierIndex + 1; QRCodeInfo fallback = ResolveFallbackConfig(defaults, info.tier, sameTierIndex); merged.Add(MergeQRCodeInfo(info, fallback)); } return merged; } private static QRCodeInfo ResolveFallbackConfig(List defaults, QRTier tier, int sameTierIndex) { if (defaults == null || defaults.Count == 0) { return default; } int exactTierSeen = 0; for (int i = 0; i < defaults.Count; i++) { if (defaults[i].tier != tier) continue; if (exactTierSeen == sameTierIndex) return defaults[i]; exactTierSeen++; } int noneTierSeen = 0; for (int i = 0; i < defaults.Count; i++) { if (defaults[i].tier != QRTier.None) continue; if (noneTierSeen == sameTierIndex) return defaults[i]; noneTierSeen++; } return defaults[0]; } private static QRCodeInfo MergeQRCodeInfo(QRCodeInfo stored, QRCodeInfo fallback) { QRCodeInfo merged = stored; if (string.IsNullOrWhiteSpace(merged.linkPrefix)) { merged.linkPrefix = fallback.linkPrefix; } if (string.IsNullOrWhiteSpace(merged.messageFormat) || (merged.messageFormat == "{0}" && !string.IsNullOrWhiteSpace(fallback.messageFormat) && fallback.messageFormat != "{0}")) { merged.messageFormat = fallback.messageFormat; } return merged; } private static bool AreQRCodeConfigsDifferent(List left, List right) { if (ReferenceEquals(left, right)) return false; int leftCount = left != null ? left.Count : 0; int rightCount = right != null ? right.Count : 0; if (leftCount != rightCount) return true; for (int i = 0; i < leftCount; i++) { QRCodeInfo a = left[i]; QRCodeInfo b = right[i]; if (a.generateQRCode != b.generateQRCode || a.tier != b.tier || a.linkPrefix != b.linkPrefix || a.messageFormat != b.messageFormat || a.prizeText != b.prizeText || a.darkColor != b.darkColor || a.lightColor != b.lightColor) { return true; } } return false; } private void GenerateQRCodes() { _generatedQRCodes.Clear(); _qrCodesCacheByTier.Clear(); foreach (QRCodeInfo data in qrCodes) { if (!data.generateQRCode) continue; string fullLink = data.GetFullLink(_phoneNumber); Texture2D tex = QRMaker.MakeTexture( fullLink, 8, true, QRCodeInfo.GetColor(data.darkColor), QRCodeInfo.GetColor(data.lightColor)); _generatedQRCodes.Add(new GeneratedQRCodeEntry(data.tier, tex)); } } // ────────────── Настройки ────────────── private void SubscribeToSettings() { var settings = SettingsManager.Instance; if (settings != null) settings.OnSettingChanged += OnSettingChanged; } private void UnsubscribeFromSettings() { var settings = SettingsManager.Instance; if (settings != null) settings.OnSettingChanged -= OnSettingChanged; } private void OnSettingChanged(string key, SettingValue value) { if (key == "prizes.phoneNumber") { RefreshPhoneNumber(); GenerateQRCodes(); Debug.Log("[PrizeSystemManager] QR-коды перегенерированы."); } } private static string GetCurrentLevelName() { if (GameManager.Instance == null) return string.Empty; return $"{GameManager.Instance.World}-{GameManager.Instance.Stage}"; } private readonly struct GeneratedQRCodeEntry { public readonly QRTier Tier; public readonly Texture2D Texture; public GeneratedQRCodeEntry(QRTier tier, Texture2D texture) { Tier = tier; Texture = texture; } } } public readonly struct PrizeStandConnectionInfo { public readonly bool IsConnected; public readonly int ConnectedBoxesCount; public PrizeStandConnectionInfo(bool isConnected, int connectedBoxesCount) { IsConnected = isConnected; ConnectedBoxesCount = connectedBoxesCount; } } }