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;
}
}
}