using System; using System.Collections.Generic; using System.Data; using System.IO; using UnityEngine; using SqliteConnection = Microsoft.Data.Sqlite.SqliteConnection; public class PlayerStatsService : IDisposable { private string _connectionString; private IDbConnection _dbConnection; private string dbPath; public PlayerStatsService() { dbPath = Path.Combine(Application.persistentDataPath, "player_stats.db"); _connectionString = $"Data Source={dbPath};Cache=Shared;Mode=ReadWriteCreate"; } // Инициализация базы данных и создание таблицы public void Initialize() { try { bool isNewDatabase = !DatabaseExists(); SQLitePCL.Batteries_V2.Init(); _dbConnection = new SqliteConnection(_connectionString); _dbConnection.Open(); CreateTableIfNotExists(); if (isNewDatabase) { InitialRecords(8); } } catch (Exception ex) { Debug.LogError($"Failed to initialize PlayerStatsService: {ex.Message}"); throw; } } private bool DatabaseExists() { return File.Exists(dbPath); } private void CreateTableIfNotExists() { string createTableQuery = @" CREATE TABLE IF NOT EXISTS player_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_name TEXT NOT NULL, completion_time_sec REAL NOT NULL, moves_count INTEGER NOT NULL, completed_at DATETIME NOT NULL, is_synced BOOLEAN NOT NULL DEFAULT 0, is_bot BOOLEAN NOT NULL DEFAULT 0 -- Новое поле )"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = createTableQuery; dbCommand.ExecuteNonQuery(); } CreateIndexes(); } private void CreateIndexes() { string[] indexQueries = { "CREATE INDEX IF NOT EXISTS idx_time_moves ON player_stats (completion_time_sec, moves_count)", "CREATE INDEX IF NOT EXISTS idx_completed_at ON player_stats (completed_at)" }; foreach (string query in indexQueries) { using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = query; dbCommand.ExecuteNonQuery(); } } } // Добавление новой записи public void AddRecord(PlayerStatsRecord record) { if (record == null) throw new ArgumentNullException(nameof(record)); string insertQuery = @" INSERT INTO player_stats (player_name, completion_time_sec, moves_count, completed_at, is_synced, is_bot) VALUES (@playerName, @completionTime, @movesCount, @completedAt, @isSynced, @isBot)"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = insertQuery; AddParameter(dbCommand, "@playerName", record.PlayerName); AddParameter(dbCommand, "@completionTime", record.CompletionTimeSec); AddParameter(dbCommand, "@movesCount", record.MovesCount); AddParameter(dbCommand, "@completedAt", record.CompletedAt); AddParameter(dbCommand, "@isSynced", record.IsSynced); AddParameter(dbCommand, "@isBot", record.IsBot); // Новый параметр dbCommand.ExecuteNonQuery(); } } // Удобный метод для добавления записи без создания объекта вручную public void AddRecord(string playerName, float completionTimeSec, int movesCount, bool isBot = false) { var record = new PlayerStatsRecord(playerName, completionTimeSec, movesCount, isBot); AddRecord(record); } // Получение N лучших игроков public List GetTopPlayers(int count = 3, bool includeBots = true) { var records = new List(); string whereClause = includeBots ? "" : " WHERE is_bot = 0"; string selectQuery = $@" SELECT id, player_name, completion_time_sec, moves_count, completed_at, is_synced, is_bot FROM player_stats {whereClause} ORDER BY completion_time_sec ASC, moves_count ASC LIMIT @limit"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = selectQuery; AddParameter(dbCommand, "@limit", count); using (IDataReader reader = dbCommand.ExecuteReader()) { while (reader.Read()) { records.Add(CreateRecordFromReader(reader)); } } } return records; } // Получение места записи в рейтинге public int GetRecordRank(PlayerStatsRecord record) { if (record == null) throw new ArgumentNullException(nameof(record)); string rankQuery = @" SELECT COUNT(*) + 1 as rank FROM player_stats WHERE completion_time_sec < @completionTime OR (completion_time_sec = @completionTime AND moves_count < @movesCount)"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = rankQuery; AddParameter(dbCommand, "@completionTime", record.CompletionTimeSec); AddParameter(dbCommand, "@movesCount", record.MovesCount); var result = dbCommand.ExecuteScalar(); return Convert.ToInt32(result); } } // Получение общего количества записей public int GetTotalRecordsCount() { string countQuery = "SELECT COUNT(*) FROM player_stats"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = countQuery; var result = dbCommand.ExecuteScalar(); return Convert.ToInt32(result); } } public PlayerStatsRecord GetLatestRecord() { string selectQuery = @" SELECT id, player_name, completion_time_sec, moves_count, completed_at, is_synced, is_bot FROM player_stats ORDER BY completed_at DESC LIMIT 1"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = selectQuery; using (IDataReader reader = dbCommand.ExecuteReader()) { if (reader.Read()) { return CreateRecordFromReader(reader); } } } return null; // Если записей нет } // Вспомогательный метод для создания записи из DataReader private PlayerStatsRecord CreateRecordFromReader(IDataReader reader) { return new PlayerStatsRecord { Id = reader.GetInt32(0), PlayerName = reader.GetString(1), CompletionTimeSec = reader.GetFloat(2), // Изменено с GetInt32 на GetFloat MovesCount = reader.GetInt32(3), CompletedAt = reader.GetDateTime(4), IsSynced = reader.GetBoolean(5), IsBot = reader.GetBoolean(6) // Новое поле }; } // Вспомогательный метод для добавления параметров private void AddParameter(IDbCommand command, string parameterName, object value) { var p = command.CreateParameter(); p.ParameterName = parameterName; if (value is DateTime dt) { p.DbType = DbType.DateTime; // хранится как TEXT (ISO8601) у Microsoft.Data.Sqlite — читаться будет через GetDateTime p.Value = dt; } else if (value is bool b) { p.DbType = DbType.Boolean; // в SQLite это 0/1 p.Value = b; } else { p.Value = value ?? DBNull.Value; } command.Parameters.Add(p); } // Очистка всех записей (для тестирования) public void ClearAllRecords() { string deleteQuery = "DELETE FROM player_stats"; using (IDbCommand dbCommand = _dbConnection.CreateCommand()) { dbCommand.CommandText = deleteQuery; dbCommand.ExecuteNonQuery(); } } // Закрытие соединения public void Dispose() { _dbConnection?.Close(); _dbConnection?.Dispose(); } private void InitialRecords(int count) { List highScoreNames = new List { "ПОБЕДИТЕЛЬ", "ЛЕГЕНДА", "ЧЕМПИОН", "МАСТЕР", "ГЕРОЙ", "ЦАРЬ ГОРЫ", "УМНЯШКА300", "НЕЗНАКОМЕЦ", "ТЕНЬ", "НОМЕР_007", "ОМЕГА", "АБСОЛЮТ", "ТЕРМИНАТОР", "БЕЗУМНЫЙ_М", }; if (count > highScoreNames.Count) { count = highScoreNames.Count; Debug.Log("Запрогено больше имен, чем есть в списке. Взято количество доступных имен."); } for (int i = 0; i < count; i++) { string name = highScoreNames[UnityEngine.Random.Range(0, highScoreNames.Count)]; highScoreNames.Remove(name); int moves = UnityEngine.Random.Range(60, 200); float time = UnityEngine.Random.Range(120f, 800f); DateTime date = GetRandomDateTimeInRange(); // Все начальные записи - боты PlayerStatsRecord record = new PlayerStatsRecord(name, time, moves, date, true); AddRecord(record); } } public void AddDailyRecord() { List highScoreNames = new List { "АНЯ", "ВАСЯ", "КОЛЯ", "МИША", "ТАНЯ", "СВЕТА", "ИГОРЬ", "ОЛЕГ", "МАКСИМ", "ДИМА", "ЛЕХА ТОП", "ДАНЯ ПРО", "ААА", "ГРОЗА", "ЧАЙ", "Я", "РОБЛОКС", "НАГИБ", "БОБО", "", }; string name = highScoreNames[UnityEngine.Random.Range(0, highScoreNames.Count)]; int moves = UnityEngine.Random.Range(60, 200); float time = UnityEngine.Random.Range(120f, 800f); DateTime date = DateTime.Now; PlayerStatsRecord record = new PlayerStatsRecord(name, time, moves, date, true); AddRecord(record); } /* public static DateTime GetRandomDateTimeInRange() { System.Random random = new System.Random(); // Диапазон дней: от сегодня-5 до сегодня DateTime today = DateTime.Today; DateTime minDate = today.AddDays(-5); int daysRange = (today - minDate).Days; DateTime randomDay = minDate.AddDays(random.Next(daysRange + 1)); // +1 чтобы включить сегодня // Диапазон часов: от 11 до 17 int randomHour = random.Next(11, 18); // 18 не включается, так что получим 11-17 int randomMinute = random.Next(0, 60); int randomSecond = random.Next(0, 60); return new DateTime(randomDay.Year, randomDay.Month, randomDay.Day, randomHour, randomMinute, randomSecond); }*/ public static DateTime GetRandomDateTimeInRange() { System.Random random = new System.Random(); DateTime now = DateTime.Now; // Максимальная дата/время: {сейчас} - 5 часов DateTime maxDateTime = now.AddHours(-5); // Если время maxDateTime выходит за рамки 11-17 часов, корректируем if (maxDateTime.Hour < 11) { // Если меньше 11 утра, смещаем на предыдущий день 17:00 maxDateTime = maxDateTime.Date.AddDays(-1).AddHours(17); } else if (maxDateTime.Hour > 17) { // Если больше 17:00, устанавливаем на сегодня 17:00 maxDateTime = maxDateTime.Date.AddHours(17); } // Минимальная дата: 5 дней назад от maxDateTime DateTime minDateTime = maxDateTime.AddDays(-5); // Корректируем minDateTime, чтобы он тоже был в диапазоне 11-17 часов if (minDateTime.Hour < 11) { minDateTime = minDateTime.Date.AddHours(11); } else if (minDateTime.Hour > 17) { minDateTime = minDateTime.Date.AddHours(17); } // Генерируем случайные даты в диапазоне с учетом часов 11-17 List validDates = new List(); // Перебираем все дни в диапазоне for (DateTime date = minDateTime.Date; date <= maxDateTime.Date; date = date.AddDays(1)) { // Для каждого дня добавляем только часы с 11 до 17 for (int hour = 11; hour <= 17; hour++) { DateTime candidate = date.AddHours(hour).AddMinutes(random.Next(0, 60)).AddSeconds(random.Next(0, 60)); // Проверяем, что время находится в нашем диапазоне if (candidate >= minDateTime && candidate <= maxDateTime) { validDates.Add(candidate); } } } // Если есть подходящие даты, возвращаем случайную if (validDates.Count > 0) { return validDates[random.Next(validDates.Count)]; } // Если нет подходящих дат (очень узкий диапазон), возвращаем minDateTime return minDateTime; } }