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", "ОМЕГА", "АБСОЛЮТ", "ТЕРМИНАТОР", "БЕЗУМНЫЙ_М", }; for (int i = 0; i < count; i++) { string name = highScoreNames[UnityEngine.Random.Range(0, highScoreNames.Count)]; highScoreNames.Remove(name); int moves = UnityEngine.Random.Range(20, 80); float time = UnityEngine.Random.Range(60f, 500f); DateTime date = GetRandomDateTimeInRange(); // Все начальные записи - боты PlayerStatsRecord record = new PlayerStatsRecord(name, time, moves, date, true); AddRecord(record); } } public void AddDailyRecord() { List highScoreNames = new List { "ПОБЕДИТЕЛЬ", "ЛЕГЕНДА", "ЧЕМПИОН", "МАСТЕР", "ГЕРОЙ", "ЦАРЬ ГОРЫ", "УМНЯШКА300", "НЕЗНАКОМЕЦ", "ТЕНЬ", "НОМЕР_007", "ОМЕГА", "АБСОЛЮТ", "ТЕРМИНАТОР", "БЕЗУМНЫЙ_М", }; string name = highScoreNames[UnityEngine.Random.Range(0, highScoreNames.Count)]; int moves = UnityEngine.Random.Range(20, 80); float time = UnityEngine.Random.Range(60f, 500f); 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); } }