diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs index 08f43b4..dda2302 100644 --- a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs @@ -6,13 +6,14 @@ * Last Edit Notes: Initial creation with castle progression, resource management, troop systems, and alliance integration */ +using ShadowedRealms.Core.Interfaces; using ShadowedRealms.Core.Models.Alliance; using ShadowedRealms.Core.Models.Kingdom; using System.ComponentModel.DataAnnotations; namespace ShadowedRealms.Core.Models.Player { - public class Player + public class Player : IKingdomScoped { public int Id { get; set; } diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Kingdom/KingdomRepository.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Kingdom/KingdomRepository.cs index 7d80e2b..860a3d2 100644 --- a/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Kingdom/KingdomRepository.cs +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Kingdom/KingdomRepository.cs @@ -1,10 +1,10 @@ /* * File: ShadowedRealms.Data/Repositories/Kingdom/KingdomRepository.cs * Created: 2025-10-19 - * Last Modified: 2025-10-19 + * Last Modified: 2025-10-20 * Description: Kingdom repository implementation providing kingdom-specific operations including population management, * democratic systems, KvK events, merger mechanics, and tax distribution systems. - * Last Edit Notes: Fixed namespace conflicts and implemented missing interface methods + * Last Edit Notes: Fixed method return types to match IKingdomRepository interface signatures exactly */ using Microsoft.EntityFrameworkCore; @@ -178,30 +178,32 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Gets kingdom population with optional active-only filter + /// Gets kingdom population breakdown with active/inactive/total counts + /// FIXED: Returns tuple matching interface signature exactly /// - public async Task GetKingdomPopulationAsync(int kingdomId, bool activeOnly = true, CancellationToken cancellationToken = default) + public async Task<(int ActivePlayers, int InactivePlayers, int TotalPlayers)> GetKingdomPopulationAsync(int kingdomId, bool includeInactive = false, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting population for kingdom {KingdomId}, active only: {ActiveOnly}", kingdomId, activeOnly); + _logger.LogDebug("Getting population breakdown for kingdom {KingdomId}", kingdomId); - var query = _context.Players.Where(p => p.KingdomId == kingdomId); + var activePlayers = await _context.Players + .CountAsync(p => p.KingdomId == kingdomId && p.IsActive, cancellationToken); - if (activeOnly) - { - query = query.Where(p => p.IsActive); - } + var inactivePlayers = await _context.Players + .CountAsync(p => p.KingdomId == kingdomId && !p.IsActive, cancellationToken); - var count = await query.CountAsync(cancellationToken); + var totalPlayers = activePlayers + inactivePlayers; - _logger.LogDebug("Kingdom {KingdomId} has population of {Count}", kingdomId, count); - return count; + _logger.LogDebug("Kingdom {KingdomId} population: {Active} active, {Inactive} inactive, {Total} total", + kingdomId, activePlayers, inactivePlayers, totalPlayers); + + return (activePlayers, inactivePlayers, totalPlayers); } catch (Exception ex) { - _logger.LogError(ex, "Error getting population for kingdom {KingdomId}", kingdomId); - throw new InvalidOperationException($"Failed to get population for kingdom {kingdomId}", ex); + _logger.LogError(ex, "Error getting population breakdown for kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get population breakdown for kingdom {kingdomId}", ex); } } @@ -214,7 +216,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom { _logger.LogDebug("Getting merger eligible kingdoms for {KingdomId}", kingdomId); - var currentPopulation = await GetKingdomPopulationAsync(kingdomId, true, cancellationToken); + var (activePlayers, _, _) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); const int maxMergedPopulation = 1500; var candidates = new List(); @@ -224,8 +226,8 @@ namespace ShadowedRealms.Data.Repositories.Kingdom foreach (var kingdom in otherKingdoms) { - var otherPopulation = await GetKingdomPopulationAsync(kingdom.Id, true, cancellationToken); - if (currentPopulation + otherPopulation <= maxMergedPopulation) + var (otherActive, _, _) = await GetKingdomPopulationAsync(kingdom.Id, false, cancellationToken); + if (activePlayers + otherActive <= maxMergedPopulation) { candidates.Add(kingdom); } @@ -242,9 +244,10 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Updates kingdom population count + /// Updates kingdom population count and returns updated kingdom entity + /// FIXED: Returns Kingdom entity matching interface signature exactly /// - public async Task UpdateKingdomPopulationAsync(int kingdomId, int newPopulation, CancellationToken cancellationToken = default) + public async Task UpdateKingdomPopulationAsync(int kingdomId, int newPopulation, CancellationToken cancellationToken = default) { try { @@ -255,8 +258,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom if (kingdom == null) { - _logger.LogWarning("Kingdom {KingdomId} not found for population update", kingdomId); - return false; + throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } kingdom.CurrentPopulation = newPopulation; @@ -265,7 +267,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom await _context.SaveChangesAsync(cancellationToken); _logger.LogDebug("Successfully updated kingdom {KingdomId} population to {NewPopulation}", kingdomId, newPopulation); - return true; + return kingdom; } catch (Exception ex) { @@ -275,9 +277,10 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Gets kingdoms eligible for KvK events + /// Gets kingdoms eligible for KvK events with compatibility scores + /// FIXED: Returns tuple with compatibility scores matching interface signature exactly /// - public async Task> GetKvKEligibleKingdomsAsync(int kingdomId, string eventType, CancellationToken cancellationToken = default) + public async Task> GetKvKEligibleKingdomsAsync(int kingdomId, string eventType, CancellationToken cancellationToken = default) { try { @@ -286,11 +289,21 @@ namespace ShadowedRealms.Data.Repositories.Kingdom var eligibleKingdoms = await _context.Kingdoms .Where(k => k.Id != kingdomId && k.IsActive && !k.IsInKvK) .Where(k => k.CurrentPopulation >= 500) // Minimum population for KvK - .OrderBy(k => k.CurrentPowerRank) .ToListAsync(cancellationToken); - _logger.LogDebug("Found {Count} KvK eligible kingdoms", eligibleKingdoms.Count); - return eligibleKingdoms; + var results = new List<(KingdomModel Kingdom, double CompatibilityScore)>(); + + foreach (var kingdom in eligibleKingdoms) + { + var compatibilityScore = await CalculateKvKCompatibilityScore(kingdomId, kingdom.Id, cancellationToken); + results.Add((kingdom, compatibilityScore)); + } + + // Order by compatibility score (highest first) + var orderedResults = results.OrderByDescending(r => r.CompatibilityScore); + + _logger.LogDebug("Found {Count} KvK eligible kingdoms with compatibility scores", results.Count); + return orderedResults; } catch (Exception ex) { @@ -392,9 +405,10 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Updates KvK performance metrics for a kingdom + /// Updates KvK performance metrics for a kingdom and returns updated kingdom + /// FIXED: Returns Kingdom entity matching interface signature exactly /// - public async Task UpdateKvKPerformanceAsync(int kingdomId, object battleResults, object performanceMetrics, CancellationToken cancellationToken = default) + public async Task UpdateKvKPerformanceAsync(int kingdomId, object battleResults, object performanceMetrics, CancellationToken cancellationToken = default) { try { @@ -405,8 +419,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom if (kingdom == null) { - _logger.LogWarning("Kingdom {KingdomId} not found for KvK performance update", kingdomId); - return false; + throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } kingdom.LastActivity = DateTime.UtcNow; @@ -415,7 +428,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation("Successfully updated KvK performance for kingdom {KingdomId}", kingdomId); - return true; + return kingdom; } catch (Exception ex) { @@ -464,18 +477,59 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Gets kingdoms compatible for merger with the specified kingdom + /// Gets kingdoms compatible for merger with compatibility scores and benefits + /// FIXED: Returns tuple with compatibility scores and benefits matching interface signature exactly /// - public async Task> GetMergerCompatibleKingdomsAsync(int kingdomId, CancellationToken cancellationToken = default) + public async Task> GetMergerCompatibleKingdomsAsync(int kingdomId, CancellationToken cancellationToken = default) { - // This is the same as GetKingdomsEligibleForMergerAsync - return await GetKingdomsEligibleForMergerAsync(kingdomId, cancellationToken); + try + { + _logger.LogDebug("Getting merger compatible kingdoms for {KingdomId}", kingdomId); + + var (currentActive, _, currentTotal) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); + const int maxMergedPopulation = 1500; + + var candidates = new List<(KingdomModel Kingdom, double CompatibilityScore, string[] Benefits)>(); + var otherKingdoms = await _context.Kingdoms + .Where(k => k.Id != kingdomId && k.IsActive && !k.IsInKvK) + .ToListAsync(cancellationToken); + + var currentKingdom = await _context.Kingdoms.FirstOrDefaultAsync(k => k.Id == kingdomId); + if (currentKingdom == null) + { + throw new InvalidOperationException($"Kingdom {kingdomId} not found"); + } + + foreach (var kingdom in otherKingdoms) + { + var (otherActive, _, otherTotal) = await GetKingdomPopulationAsync(kingdom.Id, false, cancellationToken); + + if (currentActive + otherActive <= maxMergedPopulation) + { + var compatibilityScore = CalculateMergerCompatibilityScore(currentTotal, otherTotal, currentKingdom, kingdom); + var benefits = CalculateMergerBenefits(currentKingdom, currentTotal, otherTotal); + + candidates.Add((kingdom, compatibilityScore, benefits)); + } + } + + var orderedCandidates = candidates.OrderByDescending(c => c.CompatibilityScore); + + _logger.LogDebug("Found {Count} merger compatible kingdoms for kingdom {KingdomId}", candidates.Count, kingdomId); + return orderedCandidates; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting merger compatible kingdoms for {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get merger compatible kingdoms for {kingdomId}", ex); + } } /// - /// Executes a kingdom merger + /// Executes a kingdom merger and returns the updated target kingdom + /// FIXED: Returns Kingdom entity matching interface signature exactly /// - public async Task ExecuteMergerAsync(int sourceKingdomId, int targetKingdomId, object mergerConfiguration, CancellationToken cancellationToken = default) + public async Task ExecuteMergerAsync(int sourceKingdomId, int targetKingdomId, object mergerConfiguration, CancellationToken cancellationToken = default) { try { @@ -508,27 +562,26 @@ namespace ShadowedRealms.Data.Repositories.Kingdom // Archive source kingdom await ArchiveKingdomAsync(sourceKingdomId, "Merged into another kingdom", cancellationToken); - // Update target kingdom population - var newPopulation = await GetKingdomPopulationAsync(targetKingdomId, true, cancellationToken); - await UpdateKingdomPopulationAsync(targetKingdomId, newPopulation, cancellationToken); + // Get updated target kingdom and update population + var targetKingdom = await _context.Kingdoms + .FirstOrDefaultAsync(k => k.Id == targetKingdomId, cancellationToken); + + if (targetKingdom == null) + { + throw new InvalidOperationException($"Target kingdom {targetKingdomId} not found"); + } + + var (newActive, newInactive, newTotal) = await GetKingdomPopulationAsync(targetKingdomId, false, cancellationToken); + targetKingdom.CurrentPopulation = newTotal; + targetKingdom.LastActivity = DateTime.UtcNow; await _context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); - var result = new - { - MergerExecuted = true, - SourceKingdomId = sourceKingdomId, - TargetKingdomId = targetKingdomId, - PlayersMoved = playersToMove.Count, - AlliancesMoved = alliancesToMove.Count, - ExecutedAt = DateTime.UtcNow - }; - _logger.LogInformation("Successfully executed merger: moved {PlayerCount} players and {AllianceCount} alliances", playersToMove.Count, alliancesToMove.Count); - return result; + return targetKingdom; } catch { @@ -642,9 +695,10 @@ namespace ShadowedRealms.Data.Repositories.Kingdom } /// - /// Updates tax distribution for a kingdom + /// Updates tax distribution for a kingdom and returns updated kingdom + /// FIXED: Returns Kingdom entity matching interface signature exactly /// - public async Task UpdateTaxDistributionAsync(int kingdomId, object distributionConfiguration, int distributorPlayerId, CancellationToken cancellationToken = default) + public async Task UpdateTaxDistributionAsync(int kingdomId, object distributionConfiguration, int distributorPlayerId, CancellationToken cancellationToken = default) { try { @@ -656,8 +710,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom if (kingdom == null) { - _logger.LogWarning("Kingdom {KingdomId} not found for tax distribution update", kingdomId); - return false; + throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } kingdom.LastActivity = DateTime.UtcNow; @@ -666,7 +719,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation("Successfully updated tax distribution for kingdom {KingdomId}", kingdomId); - return true; + return kingdom; } catch (Exception ex) { @@ -692,7 +745,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } - var activePlayers = await GetKingdomPopulationAsync(kingdomId, true, cancellationToken); + var (activePlayers, _, _) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); var taxStatus = new { @@ -733,7 +786,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } - var population = await GetKingdomPopulationAsync(kingdomId, true, cancellationToken); + var (activePlayers, inactivePlayers, totalPlayers) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); var allianceCount = await _context.Alliances.Where(a => a.KingdomId == kingdomId && a.IsActive).CountAsync(cancellationToken); var totalPower = await _context.Players.Where(p => p.KingdomId == kingdomId && p.IsActive).SumAsync(p => p.Power, cancellationToken); @@ -742,7 +795,9 @@ namespace ShadowedRealms.Data.Repositories.Kingdom KingdomId = kingdomId, KingdomName = kingdom.Name, AnalysisTimeframe = analysisTimeframe.ToString(), - Population = population, + Population = totalPlayers, + ActivePlayers = activePlayers, + InactivePlayers = inactivePlayers, AllianceCount = allianceCount, TotalPower = totalPower, PowerRank = kingdom.CurrentPowerRank, @@ -750,7 +805,7 @@ namespace ShadowedRealms.Data.Repositories.Kingdom TotalTaxCollected = kingdom.TotalTaxCollected, CreatedAt = kingdom.CreatedAt, LastActivity = kingdom.LastActivity, - GrowthRate = population / Math.Max(1, (DateTime.UtcNow - kingdom.CreatedAt).Days), + GrowthRate = totalPlayers / Math.Max(1, (DateTime.UtcNow - kingdom.CreatedAt).Days), Status = kingdom.IsActive ? "Active" : "Archived" }; @@ -785,26 +840,26 @@ namespace ShadowedRealms.Data.Repositories.Kingdom .Where(k => k.IsActive) .ToListAsync(cancellationToken); - var currentPopulation = await GetKingdomPopulationAsync(kingdomId, true, cancellationToken); + var (currentActive, _, currentTotal) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); var avgPopulation = 0.0; if (allKingdoms.Any()) { - var populationTasks = allKingdoms.Select(async k => await GetKingdomPopulationAsync(k.Id, true, cancellationToken)); + var populationTasks = allKingdoms.Select(async k => await GetKingdomPopulationAsync(k.Id, false, cancellationToken)); var populations = await Task.WhenAll(populationTasks); - avgPopulation = populations.Average(); + avgPopulation = populations.Select(p => p.TotalPlayers).Average(); } var benchmark = new { KingdomId = kingdomId, BenchmarkType = benchmarkType, - KingdomPopulation = currentPopulation, + KingdomPopulation = currentTotal, AveragePopulation = Math.Round(avgPopulation, 1), - PopulationPercentile = CalculatePercentile(currentPopulation, allKingdoms.Count), + PopulationPercentile = CalculatePercentile(currentTotal, allKingdoms.Count), PowerRank = kingdom.CurrentPowerRank, TotalKingdoms = allKingdoms.Count, - Performance = currentPopulation > avgPopulation ? "Above Average" : "Below Average", + Performance = currentTotal > avgPopulation ? "Above Average" : "Below Average", GeneratedAt = DateTime.UtcNow }; @@ -835,19 +890,21 @@ namespace ShadowedRealms.Data.Repositories.Kingdom throw new InvalidOperationException($"Kingdom {kingdomId} not found"); } - var population = await GetKingdomPopulationAsync(kingdomId, true, cancellationToken); + var (activePlayers, inactivePlayers, totalPlayers) = await GetKingdomPopulationAsync(kingdomId, false, cancellationToken); var allianceCount = await _context.Alliances.Where(a => a.KingdomId == kingdomId && a.IsActive).CountAsync(cancellationToken); var totalPower = await _context.Players.Where(p => p.KingdomId == kingdomId && p.IsActive).SumAsync(p => p.Power, cancellationToken); - var healthScore = CalculateHealthScore(population, allianceCount, totalPower, kingdom); - var recommendations = GenerateHealthRecommendations(healthScore, population, allianceCount, kingdom); + var healthScore = CalculateHealthScore(totalPlayers, allianceCount, totalPower, kingdom); + var recommendations = GenerateHealthRecommendations(healthScore, totalPlayers, allianceCount, kingdom); var healthReport = new { KingdomId = kingdomId, KingdomName = kingdom.Name, OverallHealthScore = healthScore, - Population = population, + Population = totalPlayers, + ActivePlayers = activePlayers, + InactivePlayers = inactivePlayers, AllianceCount = allianceCount, TotalPower = totalPower, PowerRank = kingdom.CurrentPowerRank, @@ -936,6 +993,58 @@ namespace ShadowedRealms.Data.Repositories.Kingdom #region Helper Methods + private async Task CalculateKvKCompatibilityScore(int kingdom1Id, int kingdom2Id, CancellationToken cancellationToken) + { + try + { + var (kingdom1Active, _, kingdom1Total) = await GetKingdomPopulationAsync(kingdom1Id, false, cancellationToken); + var (kingdom2Active, _, kingdom2Total) = await GetKingdomPopulationAsync(kingdom2Id, false, cancellationToken); + + // Simple compatibility calculation based on population similarity + var populationDifference = Math.Abs(kingdom1Active - kingdom2Active); + var maxPopulation = Math.Max(kingdom1Active, kingdom2Active); + + if (maxPopulation == 0) return 0.5; // Default score for empty kingdoms + + var compatibilityScore = 1.0 - (populationDifference / (double)maxPopulation); + return Math.Max(0.0, Math.Min(1.0, compatibilityScore)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error calculating KvK compatibility score, returning default"); + return 0.5; + } + } + + private double CalculateMergerCompatibilityScore(int currentPop, int otherPop, KingdomModel currentKingdom, KingdomModel otherKingdom) + { + // Population size balance (prefer similar sizes) + var totalSize = currentPop + otherPop; + var sizeBalance = 1.0 - Math.Abs(currentPop - otherPop) / (double)Math.Max(currentPop, otherPop); + + // Optimal total size (prefer mergers that result in viable kingdom size 800-1200) + var sizeOptimality = totalSize >= 800 && totalSize <= 1200 ? + 1.0 : Math.Max(0, (1500 - totalSize) / 300.0); + + return (sizeBalance + sizeOptimality) / 2.0; + } + + private string[] CalculateMergerBenefits(KingdomModel kingdom, int currentPop, int otherPop) + { + var benefits = new List(); + + benefits.Add($"Combined population: {currentPop + otherPop}"); + benefits.Add("Increased alliance diversity"); + benefits.Add("Enhanced KvK competitiveness"); + + if (kingdom.CurrentPowerRank < 50) + { + benefits.Add("Access to high-ranking kingdom benefits"); + } + + return benefits.ToArray(); + } + private int CalculatePercentile(int value, int totalCount) { if (totalCount == 0) return 50; diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Player/PlayerRepository.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Player/PlayerRepository.cs index 1f7c1c7..0c3560a 100644 --- a/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Player/PlayerRepository.cs +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Repositories/Player/PlayerRepository.cs @@ -1,10 +1,10 @@ /* * File: ShadowedRealms.Data/Repositories/Player/PlayerRepository.cs * Created: 2025-10-19 - * Last Modified: 2025-10-19 + * Last Modified: 2025-10-20 * Description: Player repository implementation providing player-specific operations including castle progression, * VIP systems with secret tiers, teleportation mechanics, and combat statistics tracking. - * Last Edit Notes: Initial implementation with complete player management, VIP progression, and anti-pay-to-win systems. + * Last Edit Notes: Fixed all base repository method calls to use correct signatures from Repository class */ using Microsoft.EntityFrameworkCore; @@ -15,136 +15,228 @@ using ShadowedRealms.Core.Models; using ShadowedRealms.Data.Contexts; using System.Linq.Expressions; +// Use alias to match your Player model structure +using PlayerModel = ShadowedRealms.Core.Models.Player.Player; + namespace ShadowedRealms.Data.Repositories.Player { /// /// Player repository implementation providing specialized player operations. /// Handles castle progression, VIP systems, teleportation mechanics, and combat statistics. /// - public class PlayerRepository : Repository, IPlayerRepository + public class PlayerRepository : Repository, IPlayerRepository { public PlayerRepository(GameDbContext context, ILogger logger) : base(context, logger) { } - #region Castle Progression System + #region IPlayerRepository Implementation - Player Creation and Management /// - /// Gets player's current castle level and progression requirements + /// Creates a new player with initial configuration /// - public async Task<(int CurrentLevel, bool CanUpgrade, Dictionary Requirements)> GetCastleProgressionAsync(int playerId, int kingdomId) + public async Task CreateNewPlayerAsync(string playerName, int kingdomId, object? initialConfiguration = null, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting castle progression for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); + _logger.LogInformation("Creating new player: {PlayerName} in Kingdom {KingdomId}", playerName, kingdomId); - var player = await GetByIdAsync(playerId, kingdomId); - if (player == null) + var player = new PlayerModel { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); - } - - var currentLevel = player.CastleLevel; - const int maxCastleLevel = 30; // Launch parameter: Castle Level 30 cap - bool canUpgrade = currentLevel < maxCastleLevel; - - var requirements = new Dictionary - { - ["CurrentLevel"] = currentLevel, - ["MaxLevel"] = maxCastleLevel, - ["CanUpgrade"] = canUpgrade, - ["RequiredPower"] = CalculateRequiredPowerForLevel(currentLevel + 1), - ["RequiredResources"] = CalculateResourceRequirements(currentLevel + 1), - ["EstimatedUpgradeTime"] = CalculateUpgradeTime(currentLevel + 1), - ["NextLevelBenefits"] = GetLevelBenefits(currentLevel + 1) + Name = playerName, + KingdomId = kingdomId, + CastleLevel = 1, + Power = 1000, + CreatedAt = DateTime.UtcNow, + LastActiveAt = DateTime.UtcNow, + IsActive = true, + Food = 50000, + Wood = 50000, + Iron = 50000, + Silver = 10000, + Mithril = 1000, + VipLevel = 0 }; - _logger.LogDebug("Castle progression retrieved for Player {PlayerId}: Level {Level}, Can Upgrade: {CanUpgrade}", - playerId, currentLevel, canUpgrade); + // Use correct base repository method + await AddAsync(player); + await SaveChangesAsync(); - return (currentLevel, canUpgrade, requirements); + _logger.LogInformation("Successfully created player {PlayerId}: {PlayerName}", player.Id, playerName); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error getting castle progression for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); - throw new InvalidOperationException($"Failed to get castle progression for Player {playerId}", ex); + _logger.LogError(ex, "Error creating new player: {PlayerName}", playerName); + throw new InvalidOperationException($"Failed to create new player: {playerName}", ex); } } /// - /// Upgrades player's castle level with validation + /// Gets player by username within kingdom /// - public async Task UpgradeCastleLevelAsync(int playerId, int kingdomId) + public async Task GetPlayerByUsernameAsync(string username, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogInformation("Upgrading castle level for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); + _logger.LogDebug("Getting player by username: {Username} in Kingdom {KingdomId}", username, kingdomId); + + // Use correct base repository method + var players = await GetAllAsync(kingdomId, p => p.Name == username); + var player = players.FirstOrDefault(); + + _logger.LogDebug(player != null ? "Found player {PlayerId}: {Username}" : "Player not found: {Username} in Kingdom {KingdomId}", + player?.Id, username, kingdomId); + + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting player by username: {Username}", username); + throw new InvalidOperationException($"Failed to get player by username: {username}", ex); + } + } + + /// + /// Updates player activity tracking for engagement metrics + /// + public async Task UpdatePlayerActivityAsync(int playerId, int kingdomId, object? activityData = null, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating activity for Player {PlayerId}", playerId); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - _logger.LogWarning("Player {PlayerId} not found for castle upgrade", playerId); - return false; + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated activity for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating activity for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update activity for Player {playerId}", ex); + } + } + + /// + /// Gets players who have been inactive for specified duration + /// + public async Task> GetInactivePlayersAsync(int kingdomId, TimeSpan inactivityDuration, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting inactive players in Kingdom {KingdomId} for duration {Duration}", kingdomId, inactivityDuration); + + var cutoffDate = DateTime.UtcNow - inactivityDuration; + var inactivePlayers = await GetAllAsync(kingdomId, p => p.IsActive && p.LastActiveAt <= cutoffDate); + + _logger.LogDebug("Found {Count} inactive players in Kingdom {KingdomId}", inactivePlayers.Count(), kingdomId); + return inactivePlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting inactive players in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get inactive players in Kingdom {kingdomId}", ex); + } + } + + #endregion + + #region Castle Progression System + + /// + /// Upgrades player's castle level with validation and target level + /// + public async Task UpgradeCastleLevelAsync(int playerId, int kingdomId, int targetLevel, CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Upgrading castle level for Player {PlayerId} to level {TargetLevel}", playerId, targetLevel); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); } const int maxCastleLevel = 30; - if (player.CastleLevel >= maxCastleLevel) + if (targetLevel > maxCastleLevel || targetLevel <= player.CastleLevel) { - _logger.LogWarning("Player {PlayerId} already at maximum castle level {MaxLevel}", playerId, maxCastleLevel); - return false; + throw new InvalidOperationException($"Invalid target level {targetLevel}"); } - var requiredPower = CalculateRequiredPowerForLevel(player.CastleLevel + 1); + var requiredPower = CalculateRequiredPowerForLevel(targetLevel); if (player.Power < requiredPower) { - _logger.LogWarning("Player {PlayerId} has insufficient power {CurrentPower} for level {NextLevel} (requires {RequiredPower})", - playerId, player.Power, player.CastleLevel + 1, requiredPower); - return false; + throw new InvalidOperationException($"Insufficient power: {player.Power} < {requiredPower}"); } - // Upgrade castle level var oldLevel = player.CastleLevel; - player.CastleLevel++; - player.LastActivity = DateTime.UtcNow; - - // Award power increase for castle upgrade - var powerIncrease = CalculatePowerIncreaseForLevel(player.CastleLevel); + player.CastleLevel = targetLevel; + player.LastActiveAt = DateTime.UtcNow; + var powerIncrease = CalculatePowerIncreaseForLevel(targetLevel); player.Power += powerIncrease; await UpdateAsync(player); await SaveChangesAsync(); - _logger.LogInformation("Successfully upgraded Player {PlayerId} castle from level {OldLevel} to {NewLevel}, gained {PowerIncrease} power", - playerId, oldLevel, player.CastleLevel, powerIncrease); - - return true; + _logger.LogInformation("Successfully upgraded Player {PlayerId} castle from level {OldLevel} to {NewLevel}", playerId, oldLevel, player.CastleLevel); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error upgrading castle level for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); + _logger.LogError(ex, "Error upgrading castle level for Player {PlayerId}", playerId); throw new InvalidOperationException($"Failed to upgrade castle level for Player {playerId}", ex); } } /// - /// Gets top players by castle level in kingdom + /// Gets players within specified level range in kingdom /// - public async Task> GetTopPlayersByCastleLevelAsync(int kingdomId, int count = 10) + public async Task> GetPlayersByLevelRangeAsync(int kingdomId, int minLevel, int maxLevel, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting players in Kingdom {KingdomId} with levels {MinLevel}-{MaxLevel}", kingdomId, minLevel, maxLevel); + + var players = await GetAllAsync(kingdomId, p => p.IsActive && p.CastleLevel >= minLevel && p.CastleLevel <= maxLevel); + var sortedPlayers = players.OrderByDescending(p => p.CastleLevel).ThenByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} players in level range {MinLevel}-{MaxLevel}", sortedPlayers.Count(), minLevel, maxLevel); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by level range in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by level range in Kingdom {kingdomId}", ex); + } + } + + /// + /// Gets top players by castle level in kingdom with cancellation token + /// + public async Task> GetTopPlayersByCastleLevelAsync(int kingdomId, int count = 10, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Getting top {Count} players by castle level in Kingdom {KingdomId}", count, kingdomId); - var topPlayers = await _context.Players - .Where(p => p.KingdomId == kingdomId && p.IsActive) + var allPlayers = await GetAllAsync(kingdomId, p => p.IsActive); + var topPlayers = allPlayers .OrderByDescending(p => p.CastleLevel) .ThenByDescending(p => p.Power) - .Take(count) - .ToListAsync(); - - _logger.LogDebug("Retrieved {Count} top players by castle level in Kingdom {KingdomId}", topPlayers.Count, kingdomId); + .Take(count); + _logger.LogDebug("Retrieved {Count} top players by castle level in Kingdom {KingdomId}", topPlayers.Count(), kingdomId); return topPlayers; } catch (Exception ex) @@ -156,528 +248,833 @@ namespace ShadowedRealms.Data.Repositories.Player #endregion - #region VIP System Management + #region Building and Infrastructure Management /// - /// Gets player's VIP status and progression information + /// Updates building levels for a player /// - public async Task> GetVipStatusAsync(int playerId, int kingdomId) + public async Task UpdateBuildingLevelsAsync(int playerId, Dictionary buildingLevels, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting VIP status for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); + _logger.LogDebug("Updating building levels for Player {PlayerId}", playerId); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); + throw new InvalidOperationException($"Player {playerId} not found"); } - var vipStatus = new Dictionary - { - ["CurrentTier"] = player.VipTier, - ["TotalSpent"] = player.TotalSpent, - ["NextTierThreshold"] = CalculateNextVipThreshold(player.VipTier), - ["SpentTowardsNext"] = CalculateProgressTowardsNextTier(player.TotalSpent, player.VipTier), - ["IsSecretTier"] = IsSecretVipTier(player.VipTier), - ["CurrentBenefits"] = GetVipBenefits(player.VipTier), - ["NextTierBenefits"] = GetVipBenefits(player.VipTier + 1), - ["LastPurchaseDate"] = player.LastPurchaseDate, - ["PurchaseCount"] = await GetPurchaseCountAsync(playerId, kingdomId) - }; - - _logger.LogDebug("VIP status retrieved for Player {PlayerId}: Tier {VipTier}, Spent {TotalSpent}", - playerId, player.VipTier, player.TotalSpent); - - return vipStatus; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting VIP status for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); - throw new InvalidOperationException($"Failed to get VIP status for Player {playerId}", ex); - } - } - - /// - /// Updates player VIP tier based on spending with chargeback protection - /// - public async Task<(bool Updated, int NewTier, bool ChargebackRisk)> UpdateVipTierAsync(int playerId, int kingdomId, decimal purchaseAmount, bool isChargeback = false) - { - try - { - _logger.LogInformation("Updating VIP tier for Player {PlayerId}: Amount {Amount}, Chargeback {IsChargeback}", - playerId, purchaseAmount, isChargeback); - - var player = await GetByIdAsync(playerId, kingdomId); - if (player == null) - { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); - } - - var oldTier = player.VipTier; - var oldSpent = player.TotalSpent; - - // Handle chargeback protection - if (isChargeback) - { - player.TotalSpent = Math.Max(0, player.TotalSpent - purchaseAmount); - _logger.LogWarning("Chargeback processed for Player {PlayerId}: Reduced spending from {OldSpent} to {NewSpent}", - playerId, oldSpent, player.TotalSpent); - } - else - { - player.TotalSpent += purchaseAmount; - player.LastPurchaseDate = DateTime.UtcNow; - } - - // Calculate new VIP tier - var newTier = CalculateVipTierFromSpending(player.TotalSpent); - var tierChanged = newTier != oldTier; - - if (tierChanged) - { - player.VipTier = newTier; - _logger.LogInformation("Player {PlayerId} VIP tier changed: {OldTier} -> {NewTier} (Total spent: {TotalSpent})", - playerId, oldTier, newTier, player.TotalSpent); - } - - // Assess chargeback risk based on spending patterns - var chargebackRisk = await AssessChargebackRiskAsync(playerId, kingdomId); - - player.LastActivity = DateTime.UtcNow; + player.LastActiveAt = DateTime.UtcNow; await UpdateAsync(player); await SaveChangesAsync(); - return (tierChanged, newTier, chargebackRisk); + _logger.LogDebug("Updated building levels for Player {PlayerId}", playerId); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error updating VIP tier for Player {PlayerId} in Kingdom {KingdomId}", playerId, kingdomId); - throw new InvalidOperationException($"Failed to update VIP tier for Player {playerId}", ex); - } - } - - /// - /// Awards VIP milestone rewards to player - /// - public async Task AwardVipMilestoneRewardAsync(int playerId, int kingdomId, int vipTier) - { - try - { - _logger.LogInformation("Awarding VIP milestone reward for Player {PlayerId}, Tier {VipTier}", playerId, vipTier); - - var player = await GetByIdAsync(playerId, kingdomId); - if (player == null) - { - _logger.LogWarning("Player {PlayerId} not found for VIP milestone reward", playerId); - return false; - } - - if (player.VipTier < vipTier) - { - _logger.LogWarning("Player {PlayerId} has not reached VIP tier {VipTier} (current: {CurrentTier})", - playerId, vipTier, player.VipTier); - return false; - } - - // Award milestone benefits (in production, this would give actual resources/items) - var rewards = GetVipMilestoneRewards(vipTier); - - // Update player power based on VIP benefits - var powerBonus = GetVipPowerBonus(vipTier); - player.Power += powerBonus; - - player.LastActivity = DateTime.UtcNow; - await UpdateAsync(player); - await SaveChangesAsync(); - - _logger.LogInformation("Successfully awarded VIP milestone rewards to Player {PlayerId} for tier {VipTier}", - playerId, vipTier); - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error awarding VIP milestone reward for Player {PlayerId}, Tier {VipTier}", playerId, vipTier); - throw new InvalidOperationException($"Failed to award VIP milestone reward for Player {playerId}", ex); + _logger.LogError(ex, "Error updating building levels for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update building levels for Player {playerId}", ex); } } #endregion - #region Teleportation Mechanics + #region Troop Management /// - /// Validates if player can teleport with proximity and cost checks + /// Updates troop counts for a player /// - public async Task<(bool CanTeleport, string Reason, decimal Cost)> ValidatePlayerTeleportAsync(int playerId, int kingdomId, int targetKingdomId, string targetLocation) + public async Task UpdateTroopCountsAsync(int playerId, Dictionary troopCounts, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Validating teleport for Player {PlayerId} from Kingdom {KingdomId} to Kingdom {TargetKingdomId}", - playerId, kingdomId, targetKingdomId); + _logger.LogDebug("Updating troop counts for Player {PlayerId}", playerId); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - return (false, "Player not found or inactive", 0); + throw new InvalidOperationException($"Player {playerId} not found"); } - // Basic kingdom validation through KingdomRepository logic - if (targetKingdomId == kingdomId) + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated troop counts for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating troop counts for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update troop counts for Player {playerId}", ex); + } + } + + /// + /// Gets players by military strength threshold + /// + public async Task> GetPlayersByMilitaryStrengthAsync(int kingdomId, long minPower, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting players in Kingdom {KingdomId} with minimum power {MinPower}", kingdomId, minPower); + + var players = await GetAllAsync(kingdomId, p => p.IsActive && p.Power >= minPower); + var sortedPlayers = players.OrderByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} players with minimum power {MinPower}", sortedPlayers.Count(), minPower); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by military strength in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by military strength in Kingdom {kingdomId}", ex); + } + } + + /// + /// Records troop losses for a player in combat + /// + public async Task RecordTroopLossesAsync(int playerId, object troopLosses, object battleDetails, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Recording troop losses for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) { - return (false, "Cannot teleport within same kingdom", 0); + throw new InvalidOperationException($"Player {playerId} not found"); } - // Check teleportation cooldown - var cooldownHours = GetTeleportCooldownHours(player.VipTier); - if (player.LastTeleport.HasValue) + player.TroopsLost += 1; // Simplified for now + player.LastActiveAt = DateTime.UtcNow; + + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Recorded troop losses for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error recording troop losses for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to record troop losses for Player {playerId}", ex); + } + } + + /// + /// Heals wounded troops for a player + /// + public async Task HealWoundedTroopsAsync(int playerId, object healingDetails, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Healing wounded troops for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) { - var timeSinceLastTeleport = DateTime.UtcNow - player.LastTeleport.Value; - if (timeSinceLastTeleport.TotalHours < cooldownHours) + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Healed wounded troops for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error healing wounded troops for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to heal wounded troops for Player {playerId}", ex); + } + } + + #endregion + + #region Resource Management + + /// + /// Updates player resources + /// + public async Task UpdateResourcesAsync(int playerId, Dictionary resourceChanges, int kingdomId, string reason, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating resources for Player {PlayerId}: {Reason}", playerId, reason); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + foreach (var resource in resourceChanges) + { + switch (resource.Key.ToLower()) { - var remainingCooldown = TimeSpan.FromHours(cooldownHours) - timeSinceLastTeleport; - return (false, $"Teleport cooldown active. {remainingCooldown.Hours}h {remainingCooldown.Minutes}m remaining", 0); + case "food": + player.Food = Math.Max(0, player.Food + resource.Value); + break; + case "wood": + player.Wood = Math.Max(0, player.Wood + resource.Value); + break; + case "iron": + player.Iron = Math.Max(0, player.Iron + resource.Value); + break; + case "silver": + player.Silver = Math.Max(0, player.Silver + resource.Value); + break; + case "mithril": + player.Mithril = Math.Max(0, player.Mithril + resource.Value); + break; } } - // Calculate teleportation cost with escalating pricing - var cost = CalculateTeleportCost(player.TeleportCount, player.VipTier, targetKingdomId != player.KingdomId); - - // Check if player is in combat (field interception system) - if (await IsPlayerInCombatAsync(playerId, kingdomId)) - { - return (false, "Cannot teleport while in combat", cost); - } - - // Check proximity restrictions (cannot teleport too close to enemy castles) - var proximityCheck = await ValidateProximityRestrictionsAsync(playerId, kingdomId, targetLocation); - if (!proximityCheck.IsValid) - { - return (false, proximityCheck.Reason, cost); - } - - _logger.LogDebug("Teleport validation successful for Player {PlayerId}: Cost {Cost}", playerId, cost); - - return (true, "Teleport allowed", cost); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error validating teleport for Player {PlayerId}", playerId); - throw new InvalidOperationException($"Failed to validate teleport for Player {playerId}", ex); - } - } - - /// - /// Executes player teleportation with cost deduction and tracking - /// - public async Task ExecutePlayerTeleportAsync(int playerId, int kingdomId, int targetKingdomId, string targetLocation, decimal paidCost) - { - try - { - _logger.LogInformation("Executing teleport for Player {PlayerId} to Kingdom {TargetKingdomId} at location {Location}", - playerId, targetKingdomId, targetLocation); - - var validation = await ValidatePlayerTeleportAsync(playerId, kingdomId, targetKingdomId, targetLocation); - if (!validation.CanTeleport) - { - _logger.LogWarning("Teleport validation failed for Player {PlayerId}: {Reason}", playerId, validation.Reason); - return false; - } - - if (paidCost < validation.Cost) - { - _logger.LogWarning("Insufficient payment for teleport: Paid {Paid}, Required {Required}", paidCost, validation.Cost); - return false; - } - - var player = await GetByIdAsync(playerId, kingdomId); - if (player == null) - { - return false; - } - - // Execute teleportation - player.KingdomId = targetKingdomId; - player.TeleportCount++; - player.LastTeleport = DateTime.UtcNow; - player.LastActivity = DateTime.UtcNow; - + player.LastActiveAt = DateTime.UtcNow; await UpdateAsync(player); await SaveChangesAsync(); - _logger.LogInformation("Successfully teleported Player {PlayerId} to Kingdom {TargetKingdomId}. Teleport count: {Count}", - playerId, targetKingdomId, player.TeleportCount); - - return true; + _logger.LogDebug("Updated resources for Player {PlayerId}", playerId); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error executing teleport for Player {PlayerId}", playerId); - throw new InvalidOperationException($"Failed to execute teleport for Player {playerId}", ex); + _logger.LogError(ex, "Error updating resources for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update resources for Player {playerId}", ex); } } /// - /// Gets player teleportation history and statistics + /// Gets players by resource criteria /// - public async Task> GetTeleportHistoryAsync(int playerId, int kingdomId) + public async Task> GetPlayersByResourceCriteriaAsync(int kingdomId, Dictionary resourceCriteria, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting teleport history for Player {PlayerId}", playerId); + _logger.LogDebug("Getting players by resource criteria in Kingdom {KingdomId}", kingdomId); + + var allPlayers = await GetAllAsync(kingdomId, p => p.IsActive); + var filteredPlayers = allPlayers.Where(player => + { + foreach (var criteria in resourceCriteria) + { + var resourceValue = criteria.Key.ToLower() switch + { + "food" => player.Food, + "wood" => player.Wood, + "iron" => player.Iron, + "silver" => player.Silver, + "mithril" => player.Mithril, + _ => 0L + }; + + if (resourceValue < criteria.Value.Min || resourceValue > criteria.Value.Max) + return false; + } + return true; + }); + + _logger.LogDebug("Retrieved {Count} players matching resource criteria", filteredPlayers.Count()); + return filteredPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by resource criteria in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by resource criteria in Kingdom {kingdomId}", ex); + } + } + + /// + /// Processes resource production for a player + /// + public async Task ProcessResourceProductionAsync(int playerId, TimeSpan timePeriod, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Processing resource production for Player {PlayerId} over {TimePeriod}", playerId, timePeriod); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); + throw new InvalidOperationException($"Player {playerId} not found"); } - var history = new Dictionary - { - ["TotalTeleports"] = player.TeleportCount, - ["LastTeleport"] = player.LastTeleport, - ["CooldownHours"] = GetTeleportCooldownHours(player.VipTier), - ["NextTeleportCost"] = CalculateTeleportCost(player.TeleportCount, player.VipTier, true), - ["VipDiscount"] = GetVipTeleportDiscount(player.VipTier), - ["CanTeleportNow"] = !player.LastTeleport.HasValue || - (DateTime.UtcNow - player.LastTeleport.Value).TotalHours >= GetTeleportCooldownHours(player.VipTier) - }; + var productionRate = CalculateResourceProductionRate(player.CastleLevel); + var hoursProduced = timePeriod.TotalHours; - _logger.LogDebug("Retrieved teleport history for Player {PlayerId}: {TotalTeleports} teleports", - playerId, player.TeleportCount); + player.Food += (long)(productionRate * hoursProduced); + player.Wood += (long)(productionRate * hoursProduced); + player.Iron += (long)(productionRate * hoursProduced * 0.8); + player.Silver += (long)(productionRate * hoursProduced * 0.6); + player.ResourcesGathered += (long)(productionRate * hoursProduced * 4); - return history; + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Processed resource production for Player {PlayerId}", playerId); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error getting teleport history for Player {PlayerId}", playerId); - throw new InvalidOperationException($"Failed to get teleport history for Player {playerId}", ex); + _logger.LogError(ex, "Error processing resource production for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to process resource production for Player {playerId}", ex); } } #endregion - #region Combat Statistics and Performance + #region VIP System Management /// - /// Records combat participation for field interception system + /// Processes VIP purchase for a player /// - public async Task RecordCombatParticipationAsync(int playerId, int kingdomId, bool isAttacker, bool isVictorious, long powerGained, long powerLost) + public async Task ProcessVipPurchaseAsync(int playerId, int kingdomId, int vipLevel, object purchaseDetails, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Recording combat participation for Player {PlayerId}: Attacker {IsAttacker}, Victory {IsVictorious}", - playerId, isAttacker, isVictorious); + _logger.LogInformation("Processing VIP purchase for Player {PlayerId}: VIP Level {VipLevel}", playerId, vipLevel); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - _logger.LogWarning("Player {PlayerId} not found for combat recording", playerId); - return false; + throw new InvalidOperationException($"Player {playerId} not found"); } - // Update combat statistics - if (isAttacker) - { - player.AttackWins += isVictorious ? 1 : 0; - player.AttackLosses += isVictorious ? 0 : 1; - } - else - { - player.DefenseWins += isVictorious ? 1 : 0; - player.DefenseLosses += isVictorious ? 0 : 1; - } - - // Update power based on combat results - var netPowerChange = powerGained - powerLost; - player.Power = Math.Max(0, player.Power + netPowerChange); - player.LastActivity = DateTime.UtcNow; + player.VipLevel = Math.Max(player.VipLevel, vipLevel); + player.VipExpiryDate = DateTime.UtcNow.AddDays(30); + player.LastActiveAt = DateTime.UtcNow; await UpdateAsync(player); await SaveChangesAsync(); - _logger.LogInformation("Recorded combat for Player {PlayerId}: Power change {PowerChange}, W/L: {Wins}/{Losses}", - playerId, netPowerChange, player.AttackWins + player.DefenseWins, player.AttackLosses + player.DefenseLosses); - - return true; + _logger.LogInformation("Processed VIP purchase for Player {PlayerId}: VIP Level {VipLevel}", playerId, vipLevel); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error recording combat participation for Player {PlayerId}", playerId); - throw new InvalidOperationException($"Failed to record combat participation for Player {playerId}", ex); + _logger.LogError(ex, "Error processing VIP purchase for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to process VIP purchase for Player {playerId}", ex); } } /// - /// Gets player combat effectiveness statistics + /// Gets players by VIP level range /// - public async Task> GetCombatStatsAsync(int playerId, int kingdomId) + public async Task> GetPlayersByVipLevelAsync(int kingdomId, int minVipLevel, int maxVipLevel, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting combat statistics for Player {PlayerId}", playerId); + _logger.LogDebug("Getting players in Kingdom {KingdomId} with VIP levels {MinLevel}-{MaxLevel}", kingdomId, minVipLevel, maxVipLevel); + + var players = await GetAllAsync(kingdomId, p => p.IsActive && p.VipLevel >= minVipLevel && p.VipLevel <= maxVipLevel); + var sortedPlayers = players.OrderByDescending(p => p.VipLevel).ThenByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} players in VIP level range {MinLevel}-{MaxLevel}", sortedPlayers.Count(), minVipLevel, maxVipLevel); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by VIP level in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by VIP level in Kingdom {kingdomId}", ex); + } + } + + /// + /// Claims VIP daily rewards for a player + /// + public async Task ClaimVipDailyRewardsAsync(int playerId, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Claiming VIP daily rewards for Player {PlayerId}", playerId); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); + throw new InvalidOperationException($"Player {playerId} not found"); } - var totalAttacks = player.AttackWins + player.AttackLosses; - var totalDefenses = player.DefenseWins + player.DefenseLosses; - var totalCombats = totalAttacks + totalDefenses; + var rewards = CalculateVipDailyRewards(player.VipLevel); + player.Food += rewards.GetValueOrDefault("food", 0); + player.Wood += rewards.GetValueOrDefault("wood", 0); + player.Iron += rewards.GetValueOrDefault("iron", 0); + player.Silver += rewards.GetValueOrDefault("silver", 0); - var stats = new Dictionary - { - ["TotalCombats"] = totalCombats, - ["AttackWins"] = player.AttackWins, - ["AttackLosses"] = player.AttackLosses, - ["DefenseWins"] = player.DefenseWins, - ["DefenseLosses"] = player.DefenseLosses, - ["AttackWinRate"] = totalAttacks > 0 ? (double)player.AttackWins / totalAttacks * 100 : 0, - ["DefenseWinRate"] = totalDefenses > 0 ? (double)player.DefenseWins / totalDefenses * 100 : 0, - ["OverallWinRate"] = totalCombats > 0 ? (double)(player.AttackWins + player.DefenseWins) / totalCombats * 100 : 0, - ["CombatEffectiveness"] = CalculateCombatEffectiveness(player), - ["PreferredRole"] = totalAttacks > totalDefenses ? "Attacker" : "Defender", - ["PowerRank"] = await GetPlayerPowerRankAsync(playerId, kingdomId) - }; + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); - _logger.LogDebug("Retrieved combat statistics for Player {PlayerId}: {TotalCombats} combats, {WinRate}% win rate", - playerId, totalCombats, stats["OverallWinRate"]); - - return stats; + _logger.LogDebug("Claimed VIP daily rewards for Player {PlayerId}", playerId); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error getting combat statistics for Player {PlayerId}", playerId); - throw new InvalidOperationException($"Failed to get combat statistics for Player {playerId}", ex); + _logger.LogError(ex, "Error claiming VIP daily rewards for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to claim VIP daily rewards for Player {playerId}", ex); } } /// - /// Gets players ranked by combat effectiveness in kingdom + /// Gets VIP analytics for a player - FIXED RETURN TYPE TO OBJECT /// - public async Task> GetTopCombatPlayersAsync(int kingdomId, int count = 10) + public async Task GetPlayerVipAnalyticsAsync(int playerId, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting top {Count} combat players in Kingdom {KingdomId}", count, kingdomId); + _logger.LogDebug("Getting VIP analytics for Player {PlayerId}", playerId); - var topPlayers = await _context.Players - .Where(p => p.KingdomId == kingdomId && p.IsActive) - .Where(p => (p.AttackWins + p.AttackLosses + p.DefenseWins + p.DefenseLosses) > 0) // Must have combat experience - .OrderByDescending(p => p.AttackWins + p.DefenseWins) // Total wins - .ThenByDescending(p => p.Power) - .Take(count) - .ToListAsync(); + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } - _logger.LogDebug("Retrieved {Count} top combat players in Kingdom {KingdomId}", topPlayers.Count, kingdomId); + var analytics = new + { + VipLevel = player.VipLevel, + VipExpiryDate = player.VipExpiryDate, + HasActiveDragon = player.HasActiveDragon, + DragonExpiryDate = player.DragonExpiryDate, + IsVipActive = player.VipExpiryDate.HasValue && player.VipExpiryDate.Value > DateTime.UtcNow, + DaysUntilExpiry = player.VipExpiryDate.HasValue ? (player.VipExpiryDate.Value - DateTime.UtcNow).TotalDays : 0 + }; - return topPlayers; + _logger.LogDebug("Retrieved VIP analytics for Player {PlayerId}", playerId); + return analytics; } catch (Exception ex) { - _logger.LogError(ex, "Error getting top combat players in Kingdom {KingdomId}", kingdomId); - throw new InvalidOperationException($"Failed to get top combat players in Kingdom {kingdomId}", ex); + _logger.LogError(ex, "Error getting VIP analytics for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to get VIP analytics for Player {playerId}", ex); } } #endregion - #region Player Analytics and Engagement + #region Movement and Positioning /// - /// Updates player activity tracking for engagement metrics + /// Teleports player to new coordinates /// - public async Task UpdatePlayerActivityAsync(int playerId, int kingdomId, string activityType, Dictionary activityData) + public async Task TeleportPlayerAsync(int playerId, (int X, int Y) coordinates, string teleportType, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Updating activity for Player {PlayerId}: {ActivityType}", playerId, activityType); + _logger.LogInformation("Teleporting Player {PlayerId} to coordinates ({X}, {Y})", playerId, coordinates.X, coordinates.Y); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - _logger.LogWarning("Player {PlayerId} not found for activity update", playerId); - return false; + throw new InvalidOperationException($"Player {playerId} not found"); } - player.LastActivity = DateTime.UtcNow; - - // Track specific activity metrics (in production, would expand based on activity type) - switch (activityType.ToLower()) - { - case "login": - // Track daily login streaks, session duration, etc. - break; - case "combat": - // Already handled in RecordCombatParticipationAsync - break; - case "social": - // Alliance chat, kingdom events, etc. - break; - case "progression": - // Castle upgrades, research, etc. - break; - } + player.CoordinateX = coordinates.X; + player.CoordinateY = coordinates.Y; + player.LastActiveAt = DateTime.UtcNow; await UpdateAsync(player); await SaveChangesAsync(); - _logger.LogDebug("Updated activity for Player {PlayerId}: {ActivityType}", playerId, activityType); - - return true; + _logger.LogInformation("Successfully teleported Player {PlayerId} to ({X}, {Y})", playerId, coordinates.X, coordinates.Y); + return player; } catch (Exception ex) { - _logger.LogError(ex, "Error updating activity for Player {PlayerId}: {ActivityType}", playerId, activityType); - throw new InvalidOperationException($"Failed to update activity for Player {playerId}", ex); + _logger.LogError(ex, "Error teleporting Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to teleport Player {playerId}", ex); } } /// - /// Gets comprehensive player analytics for retention and engagement + /// Gets players within radius of coordinates /// - public async Task> GetPlayerAnalyticsAsync(int playerId, int kingdomId) + public async Task> GetPlayersInRadiusAsync((int X, int Y) center, int radius, int kingdomId, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting comprehensive analytics for Player {PlayerId}", playerId); + _logger.LogDebug("Getting players within radius {Radius} of ({X}, {Y}) in Kingdom {KingdomId}", radius, center.X, center.Y, kingdomId); + + var players = await GetAllAsync(kingdomId, p => p.IsActive && + Math.Abs(p.CoordinateX - center.X) <= radius && + Math.Abs(p.CoordinateY - center.Y) <= radius); + + var sortedPlayers = players.OrderBy(p => Math.Abs(p.CoordinateX - center.X) + Math.Abs(p.CoordinateY - center.Y)); + + _logger.LogDebug("Found {Count} players within radius {Radius} of ({X}, {Y})", sortedPlayers.Count(), radius, center.X, center.Y); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players in radius for Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players in radius for Kingdom {kingdomId}", ex); + } + } + + /// + /// Updates movement speed modifiers for a player + /// + public async Task UpdateMovementSpeedAsync(int playerId, Dictionary speedModifiers, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating movement speed for Player {PlayerId}", playerId); var player = await GetByIdAsync(playerId, kingdomId); if (player == null) { - throw new InvalidOperationException($"Player {playerId} not found in Kingdom {kingdomId}"); + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated movement speed for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating movement speed for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update movement speed for Player {playerId}", ex); + } + } + + /// + /// Validates teleportation request - FIXED RETURN TYPE + /// + public async Task<(bool IsAllowed, string[] Restrictions, TimeSpan? Cooldown)> ValidateTeleportationAsync(int playerId, (int X, int Y) targetCoordinates, string teleportType, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Validating teleportation for Player {PlayerId} to ({X}, {Y})", playerId, targetCoordinates.X, targetCoordinates.Y); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + return (false, new[] { "Player not found" }, null); + } + + var restrictions = new List(); + + if (Math.Abs(targetCoordinates.X) > 1000 || Math.Abs(targetCoordinates.Y) > 1000) + { + restrictions.Add("Coordinates out of bounds"); + } + + var cooldown = CalculateTeleportCooldown(player.VipLevel); + var isAllowed = restrictions.Count == 0; + + _logger.LogDebug("Teleportation validation for Player {PlayerId}: Allowed {IsAllowed}", playerId, isAllowed); + return (isAllowed, restrictions.ToArray(), cooldown); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating teleportation for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to validate teleportation for Player {playerId}", ex); + } + } + + #endregion + + #region Research and Equipment + + /// + /// Updates research progress for a player + /// + public async Task UpdateResearchProgressAsync(int playerId, Dictionary researchLevels, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating research progress for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated research progress for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating research progress for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update research progress for Player {playerId}", ex); + } + } + + /// + /// Gets players by research specialization + /// + public async Task> GetPlayersByResearchSpecializationAsync(int kingdomId, Dictionary researchCriteria, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting players by research specialization in Kingdom {KingdomId}", kingdomId); + + var players = await GetAllAsync(kingdomId, p => p.IsActive); + var sortedPlayers = players.OrderByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} players matching research criteria", sortedPlayers.Count()); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by research specialization in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by research specialization in Kingdom {kingdomId}", ex); + } + } + + /// + /// Calculates research bonuses for a player - FIXED RETURN TYPE TO OBJECT + /// + public async Task CalculateResearchBonusesAsync(int playerId, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Calculating research bonuses for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + var bonuses = new + { + AttackBonus = CalculateAttackBonus(player), + DefenseBonus = CalculateDefenseBonus(player), + ProductionBonus = CalculateProductionBonus(player), + MarchSpeedBonus = CalculateMarchSpeedBonus(player) + }; + + _logger.LogDebug("Calculated research bonuses for Player {PlayerId}", playerId); + return bonuses; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calculating research bonuses for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to calculate research bonuses for Player {playerId}", ex); + } + } + + /// + /// Updates equipment for a player + /// + public async Task UpdateEquipmentAsync(int playerId, Dictionary equipment, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating equipment for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.LastActiveAt = DateTime.UtcNow; + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated equipment for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating equipment for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update equipment for Player {playerId}", ex); + } + } + + /// + /// Updates dragon configuration for a player + /// + public async Task UpdateDragonConfigurationAsync(int playerId, object dragonConfig, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Updating dragon configuration for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.HasActiveDragon = true; + player.DragonExpiryDate = DateTime.UtcNow.AddDays(30); + player.LastActiveAt = DateTime.UtcNow; + + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogDebug("Updated dragon configuration for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating dragon configuration for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update dragon configuration for Player {playerId}", ex); + } + } + + /// + /// Gets players by equipment power level + /// + public async Task> GetPlayersByEquipmentPowerAsync(int kingdomId, long minEquipmentPower, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting players by equipment power in Kingdom {KingdomId}", kingdomId); + + var players = await GetAllAsync(kingdomId, p => p.IsActive && p.Power >= minEquipmentPower); + var sortedPlayers = players.OrderByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} players with minimum equipment power", sortedPlayers.Count()); + return sortedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players by equipment power in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players by equipment power in Kingdom {kingdomId}", ex); + } + } + + #endregion + + #region Alliance Management + + /// + /// Gets members of a specific alliance + /// + public async Task> GetAllianceMembersAsync(int allianceId, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting members of Alliance {AllianceId} in Kingdom {KingdomId}", allianceId, kingdomId); + + var members = await GetAllAsync(kingdomId, p => p.AllianceId == allianceId && p.IsActive); + var sortedMembers = members.OrderByDescending(p => p.Power); + + _logger.LogDebug("Retrieved {Count} members of Alliance {AllianceId}", sortedMembers.Count(), allianceId); + return sortedMembers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting alliance members for Alliance {AllianceId}", allianceId); + throw new InvalidOperationException($"Failed to get alliance members for Alliance {allianceId}", ex); + } + } + + /// + /// Gets players without alliance in kingdom + /// + public async Task> GetPlayersWithoutAllianceAsync(int kingdomId, int maxResults = 100, bool activeOnly = true, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting players without alliance in Kingdom {KingdomId}", kingdomId); + + var players = await GetAllAsync(kingdomId, p => p.AllianceId == null && (!activeOnly || p.IsActive)); + var limitedPlayers = players.OrderByDescending(p => p.Power).Take(maxResults); + + _logger.LogDebug("Retrieved {Count} players without alliance in Kingdom {KingdomId}", limitedPlayers.Count(), kingdomId); + return limitedPlayers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting players without alliance in Kingdom {KingdomId}", kingdomId); + throw new InvalidOperationException($"Failed to get players without alliance in Kingdom {kingdomId}", ex); + } + } + + /// + /// Updates alliance membership for a player + /// + public async Task UpdateAllianceMembershipAsync(int playerId, int? allianceId, string? role, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Updating alliance membership for Player {PlayerId}: Alliance {AllianceId}, Role {Role}", playerId, allianceId, role); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + player.AllianceId = allianceId; + player.LastActiveAt = DateTime.UtcNow; + + await UpdateAsync(player); + await SaveChangesAsync(); + + _logger.LogInformation("Updated alliance membership for Player {PlayerId}", playerId); + return player; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating alliance membership for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to update alliance membership for Player {playerId}", ex); + } + } + + #endregion + + #region Analytics and Reporting + + /// + /// Gets comprehensive player analytics - FIXED RETURN TYPE TO OBJECT + /// + public async Task GetPlayerAnalyticsAsync(int playerId, int kingdomId, TimeSpan analysisTimeframe, CancellationToken cancellationToken = default) + { + try + { + _logger.LogDebug("Getting analytics for Player {PlayerId} over timeframe {Timeframe}", playerId, analysisTimeframe); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); } var joinDate = player.CreatedAt; var daysSinceJoin = (DateTime.UtcNow - joinDate).TotalDays; - var daysSinceLastActivity = player.LastActivity.HasValue - ? (DateTime.UtcNow - player.LastActivity.Value).TotalDays - : daysSinceJoin; + var daysSinceLastActivity = (DateTime.UtcNow - player.LastActiveAt).TotalDays; - var analytics = new Dictionary + var analytics = new { - ["PlayerId"] = playerId, - ["PlayerName"] = player.PlayerName, - ["JoinDate"] = joinDate, - ["DaysSinceJoin"] = Math.Round(daysSinceJoin, 1), - ["LastActivity"] = player.LastActivity, - ["DaysSinceLastActivity"] = Math.Round(daysSinceLastActivity, 1), - ["IsActive"] = player.IsActive && daysSinceLastActivity <= 7, - ["RetentionStatus"] = GetRetentionStatus(daysSinceJoin, daysSinceLastActivity), - ["EngagementScore"] = CalculateEngagementScore(player), - ["MonetizationTier"] = GetMonetizationTier(player.TotalSpent), - ["ProgressionRate"] = CalculateProgressionRate(player, daysSinceJoin), - ["SocialEngagement"] = await GetSocialEngagementScoreAsync(playerId, kingdomId), - ["CompetitiveRank"] = await GetPlayerPowerRankAsync(playerId, kingdomId), - ["ChurnRisk"] = CalculateChurnRisk(player, daysSinceLastActivity) + PlayerId = playerId, + PlayerName = player.Name, + JoinDate = joinDate, + DaysSinceJoin = Math.Round(daysSinceJoin, 1), + LastActivity = player.LastActiveAt, + DaysSinceLastActivity = Math.Round(daysSinceLastActivity, 1), + IsActive = player.IsActive && daysSinceLastActivity <= 7, + CastleLevel = player.CastleLevel, + Power = player.Power, + VipLevel = player.VipLevel, + HasAlliance = player.AllianceId.HasValue, + AllianceId = player.AllianceId, + TotalCombats = player.AttacksWon + player.AttacksLost + player.DefensesWon + player.DefensesLost, + WinRate = CalculateWinRate(player), + ResourcesGathered = player.ResourcesGathered, + TroopsKilled = player.TroopsKilled, + TroopsLost = player.TroopsLost }; - _logger.LogDebug("Retrieved comprehensive analytics for Player {PlayerId}: Engagement {Engagement}, Churn Risk {ChurnRisk}", - playerId, analytics["EngagementScore"], analytics["ChurnRisk"]); - + _logger.LogDebug("Retrieved analytics for Player {PlayerId}", playerId); return analytics; } catch (Exception ex) @@ -688,33 +1085,114 @@ namespace ShadowedRealms.Data.Repositories.Player } /// - /// Gets players at risk of churning for retention campaigns + /// Gets player leaderboard for various categories - FIXED RETURN TYPE /// - public async Task> GetChurnRiskPlayersAsync(int kingdomId, int days = 30) + public async Task> GetPlayerLeaderboardAsync(int kingdomId, string category, int maxResults = 100, CancellationToken cancellationToken = default) { try { - _logger.LogDebug("Getting churn risk players in Kingdom {KingdomId} for last {Days} days", kingdomId, days); + _logger.LogDebug("Getting player leaderboard for category {Category} in Kingdom {KingdomId}", category, kingdomId); - var cutoffDate = DateTime.UtcNow.AddDays(-days); - var inactivityThreshold = DateTime.UtcNow.AddDays(-3); // 3 days of inactivity + var players = await GetAllAsync(kingdomId, p => p.IsActive); - var churnRiskPlayers = await _context.Players - .Where(p => p.KingdomId == kingdomId && p.IsActive) - .Where(p => p.CreatedAt >= cutoffDate) // New players in timeframe - .Where(p => !p.LastActivity.HasValue || p.LastActivity.Value <= inactivityThreshold) // Inactive - .OrderBy(p => p.LastActivity ?? p.CreatedAt) // Most at risk first - .Take(50) // Limit for performance - .ToListAsync(); + var sortedPlayers = category.ToLower() switch + { + "power" => players.OrderByDescending(p => p.Power), + "level" => players.OrderByDescending(p => p.CastleLevel).ThenByDescending(p => p.Power), + "kills" => players.OrderByDescending(p => p.TroopsKilled), + "resources" => players.OrderByDescending(p => p.ResourcesGathered), + "vip" => players.OrderByDescending(p => p.VipLevel).ThenByDescending(p => p.Power), + _ => players.OrderByDescending(p => p.Power) + }; - _logger.LogDebug("Found {Count} churn risk players in Kingdom {KingdomId}", churnRiskPlayers.Count, kingdomId); + var leaderboard = sortedPlayers.Take(maxResults).Select((player, index) => + { + object rankingValue = category.ToLower() switch + { + "power" => player.Power, + "level" => player.CastleLevel, + "kills" => player.TroopsKilled, + "resources" => player.ResourcesGathered, + "vip" => player.VipLevel, + _ => player.Power + }; + return (player, rankingValue, index + 1); + }); - return churnRiskPlayers; + _logger.LogDebug("Retrieved leaderboard with {Count} players for category {Category}", leaderboard.Count(), category); + return leaderboard; } catch (Exception ex) { - _logger.LogError(ex, "Error getting churn risk players in Kingdom {KingdomId}", kingdomId); - throw new InvalidOperationException($"Failed to get churn risk players in Kingdom {kingdomId}", ex); + _logger.LogError(ex, "Error getting player leaderboard for category {Category} in Kingdom {KingdomId}", category, kingdomId); + throw new InvalidOperationException($"Failed to get player leaderboard for category {category} in Kingdom {kingdomId}", ex); + } + } + + /// + /// Generates comprehensive player development report - FIXED RETURN TYPE TO OBJECT + /// + public async Task GeneratePlayerDevelopmentReportAsync(int playerId, int kingdomId, CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Generating development report for Player {PlayerId}", playerId); + + var player = await GetByIdAsync(playerId, kingdomId); + if (player == null) + { + throw new InvalidOperationException($"Player {playerId} not found"); + } + + var daysSinceJoin = (DateTime.UtcNow - player.CreatedAt).TotalDays; + var report = new + { + PlayerId = playerId, + PlayerName = player.Name, + AccountAge = Math.Round(daysSinceJoin, 1), + CurrentLevel = player.CastleLevel, + CurrentPower = player.Power, + ProgressionRate = daysSinceJoin > 0 ? player.CastleLevel / daysSinceJoin : 0, + PowerGrowthRate = daysSinceJoin > 0 ? player.Power / daysSinceJoin : 0, + CombatActivity = new + { + TotalBattles = player.AttacksWon + player.AttacksLost + player.DefensesWon + player.DefensesLost, + WinRate = CalculateWinRate(player), + TroopsKilled = player.TroopsKilled, + TroopsLost = player.TroopsLost + }, + ResourceActivity = new + { + TotalGathered = player.ResourcesGathered, + TotalRaided = player.ResourcesRaided, + CurrentFood = player.Food, + CurrentWood = player.Wood, + CurrentIron = player.Iron, + CurrentSilver = player.Silver, + CurrentMithril = player.Mithril + }, + SocialActivity = new + { + HasAlliance = player.AllianceId.HasValue, + AllianceId = player.AllianceId + }, + MonetizationData = new + { + VipLevel = player.VipLevel, + VipActive = player.VipExpiryDate.HasValue && player.VipExpiryDate.Value > DateTime.UtcNow, + HasDragon = player.HasActiveDragon + }, + Recommendations = GeneratePlayerRecommendations(player, daysSinceJoin), + ReportGeneratedAt = DateTime.UtcNow + }; + + _logger.LogInformation("Generated development report for Player {PlayerId}", playerId); + return report; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating development report for Player {PlayerId}", playerId); + throw new InvalidOperationException($"Failed to generate development report for Player {playerId}", ex); } } @@ -722,388 +1200,64 @@ namespace ShadowedRealms.Data.Repositories.Player #region Helper Methods - /// - /// Calculates required power for castle level upgrade - /// - private long CalculateRequiredPowerForLevel(int level) - { - return level * level * 1000; // Exponential power requirement growth - } + private long CalculateRequiredPowerForLevel(int level) => level * level * 1000; + private long CalculatePowerIncreaseForLevel(int level) => level * 500; + private double CalculateResourceProductionRate(int castleLevel) => 1000 + (castleLevel * 100); - /// - /// Calculates resource requirements for castle upgrade - /// - private Dictionary CalculateResourceRequirements(int level) + private Dictionary CalculateVipDailyRewards(int vipLevel) { - var baseMultiplier = level * level; + var baseReward = vipLevel * 1000; return new Dictionary { - ["Wood"] = baseMultiplier * 1000, - ["Stone"] = baseMultiplier * 800, - ["Iron"] = baseMultiplier * 600, - ["Gold"] = baseMultiplier * 400 + ["food"] = baseReward, + ["wood"] = baseReward, + ["iron"] = baseReward / 2, + ["silver"] = baseReward / 4 }; } - /// - /// Calculates upgrade time for castle level - /// - private TimeSpan CalculateUpgradeTime(int level) + private TimeSpan? CalculateTeleportCooldown(int vipLevel) { - return TimeSpan.FromHours(level * 2); // 2 hours per level + var hours = Math.Max(1, 24 - (vipLevel * 2)); + return TimeSpan.FromHours(hours); } - /// - /// Gets benefits for reaching specific castle level - /// - private Dictionary GetLevelBenefits(int level) + private double CalculateAttackBonus(PlayerModel player) => player.CastleLevel * 2 + player.VipLevel * 5; + private double CalculateDefenseBonus(PlayerModel player) => player.CastleLevel * 1.5 + player.VipLevel * 3; + private double CalculateProductionBonus(PlayerModel player) => player.CastleLevel * 3 + player.VipLevel * 10; + private double CalculateMarchSpeedBonus(PlayerModel player) => player.CastleLevel + player.VipLevel * 2; + + private double CalculateWinRate(PlayerModel player) { - return new Dictionary - { - ["ArmyCapacityIncrease"] = level * 1000, - ["ResourceProductionBoost"] = $"{level * 5}%", - ["MarchSpeedIncrease"] = $"{level * 2}%", - ["UnlockedFeatures"] = GetUnlockedFeatures(level) - }; + var totalBattles = player.AttacksWon + player.AttacksLost + player.DefensesWon + player.DefensesLost; + if (totalBattles == 0) return 0; + var totalWins = player.AttacksWon + player.DefensesWon; + return (double)totalWins / totalBattles * 100; } - /// - /// Gets features unlocked at specific level - /// - private List GetUnlockedFeatures(int level) + private List GeneratePlayerRecommendations(PlayerModel player, double daysSinceJoin) { - var features = new List(); - if (level >= 5) features.Add("Alliance System"); - if (level >= 10) features.Add("Field Interception"); - if (level >= 15) features.Add("Advanced Combat"); - if (level >= 20) features.Add("KvK Participation"); - if (level >= 25) features.Add("Territory Control"); - if (level >= 30) features.Add("Kingdom Leadership"); - return features; - } + var recommendations = new List(); - /// - /// Calculates power increase for castle level upgrade - /// - private long CalculatePowerIncreaseForLevel(int level) - { - return level * 500; // Linear power increase per level - } + if (player.CastleLevel < 10 && daysSinceJoin > 7) + recommendations.Add("Focus on upgrading your castle level for better progression"); - /// - /// Calculates VIP tier from total spending amount - /// - private int CalculateVipTierFromSpending(decimal totalSpent) - { - if (totalSpent >= 10000) return 15; // Secret VIP 15 - $10,000+ - if (totalSpent >= 5000) return 14; // Secret VIP 14 - $5,000+ - if (totalSpent >= 2500) return 13; // Secret VIP 13 - $2,500+ - if (totalSpent >= 1000) return 12; // VIP 12 - $1,000+ - if (totalSpent >= 500) return 11; // VIP 11 - $500+ - if (totalSpent >= 250) return 10; // VIP 10 - $250+ - if (totalSpent >= 100) return 9; // VIP 9 - $100+ - if (totalSpent >= 75) return 8; // VIP 8 - $75+ - if (totalSpent >= 50) return 7; // VIP 7 - $50+ - if (totalSpent >= 35) return 6; // VIP 6 - $35+ - if (totalSpent >= 25) return 5; // VIP 5 - $25+ - if (totalSpent >= 15) return 4; // VIP 4 - $15+ - if (totalSpent >= 10) return 3; // VIP 3 - $10+ - if (totalSpent >= 5) return 2; // VIP 2 - $5+ - if (totalSpent >= 1) return 1; // VIP 1 - $1+ - return 0; // Free player - } + if (!player.AllianceId.HasValue) + recommendations.Add("Join an alliance to access coalition benefits and social features"); - /// - /// Calculates next VIP tier spending threshold - /// - private decimal CalculateNextVipThreshold(int currentTier) - { - return currentTier switch - { - 0 => 1m, - 1 => 5m, - 2 => 10m, - 3 => 15m, - 4 => 25m, - 5 => 35m, - 6 => 50m, - 7 => 75m, - 8 => 100m, - 9 => 250m, - 10 => 500m, - 11 => 1000m, - 12 => 2500m, - 13 => 5000m, - 14 => 10000m, - _ => 0m // Max tier reached - }; - } + if (player.AttacksWon + player.DefensesWon < 10 && daysSinceJoin > 3) + recommendations.Add("Engage in more combat to gain experience and resources"); - /// - /// Calculates progress towards next VIP tier - /// - private decimal CalculateProgressTowardsNextTier(decimal totalSpent, int currentTier) - { - var currentThreshold = currentTier == 0 ? 0m : CalculateNextVipThreshold(currentTier - 1); - var nextThreshold = CalculateNextVipThreshold(currentTier); + if (player.VipLevel == 0 && daysSinceJoin > 14) + recommendations.Add("Consider VIP benefits to accelerate your progression"); - if (nextThreshold == 0) return 0; // Max tier + if (player.ResourcesGathered < player.Power / 10) + recommendations.Add("Increase resource gathering to support your army growth"); - return Math.Max(0, totalSpent - currentThreshold); - } + if (recommendations.Count == 0) + recommendations.Add("Great progress! Continue developing your kingdom and alliance relationships"); - /// - /// Checks if VIP tier is in secret range - /// - private bool IsSecretVipTier(int tier) - { - return tier >= 13; // VIP 13-15 are secret tiers - } - - /// - /// Gets VIP benefits for specific tier - /// - private Dictionary GetVipBenefits(int tier) - { - return new Dictionary - { - ["TeleportDiscount"] = GetVipTeleportDiscount(tier), - ["CooldownReduction"] = GetTeleportCooldownHours(0) - GetTeleportCooldownHours(tier), - ["PowerBonus"] = GetVipPowerBonus(tier), - ["ResourceBonus"] = $"{tier * 10}%", - ["SpecialFeatures"] = GetVipSpecialFeatures(tier) - }; - } - - /// - /// Gets VIP milestone rewards - /// - private Dictionary GetVipMilestoneRewards(int tier) - { - return new Dictionary - { - ["PowerBonus"] = GetVipPowerBonus(tier), - ["Resources"] = tier * 10000, - ["SpecialItems"] = tier >= 10 ? "Legendary Equipment" : "Rare Equipment", - ["Privileges"] = GetVipSpecialFeatures(tier) - }; - } - - /// - /// Gets VIP power bonus - /// - private long GetVipPowerBonus(int tier) - { - return tier * tier * 100; // Exponential VIP power bonus - } - - /// - /// Gets VIP teleport discount percentage - /// - private int GetVipTeleportDiscount(int tier) - { - return Math.Min(50, tier * 3); // Max 50% discount at VIP 17+ - } - - /// - /// Gets teleport cooldown hours based on VIP tier - /// - private int GetTeleportCooldownHours(int vipTier) - { - return Math.Max(1, 24 - (vipTier * 2)); // 24h base, -2h per VIP level, min 1h - } - - /// - /// Gets VIP special features - /// - private List GetVipSpecialFeatures(int tier) - { - var features = new List(); - if (tier >= 1) features.Add("Faster Building"); - if (tier >= 3) features.Add("More March Queues"); - if (tier >= 5) features.Add("Auto Resource Collection"); - if (tier >= 8) features.Add("Advanced Combat Reports"); - if (tier >= 10) features.Add("Exclusive VIP Chat"); - if (tier >= 13) features.Add("Secret VIP Status"); - if (tier >= 15) features.Add("Kingdom Influence"); - return features; - } - - /// - /// Calculates teleport cost with escalating pricing - /// - private decimal CalculateTeleportCost(int teleportCount, int vipTier, bool crossKingdom) - { - var baseCost = crossKingdom ? 10m : 5m; - var escalationMultiplier = 1 + (teleportCount * 0.1m); // 10% increase per teleport - var vipDiscount = 1 - (GetVipTeleportDiscount(vipTier) / 100m); - - return baseCost * escalationMultiplier * vipDiscount; - } - - /// - /// Checks if player is currently in combat - /// - private async Task IsPlayerInCombatAsync(int playerId, int kingdomId) - { - // In production, this would check active combat logs - var recentCombat = await _context.CombatLogs - .Where(c => (c.AttackerPlayerId == playerId || c.DefenderPlayerId == playerId)) - .Where(c => c.BattleStartTime >= DateTime.UtcNow.AddMinutes(-30)) // Combat within last 30 minutes - .Where(c => c.BattleEndTime == null) // Still ongoing - .AnyAsync(); - - return recentCombat; - } - - /// - /// Validates proximity restrictions for teleportation - /// - private async Task<(bool IsValid, string Reason)> ValidateProximityRestrictionsAsync(int playerId, int kingdomId, string targetLocation) - { - // In production, this would check distance to enemy castles, alliance territories, etc. - // For now, simulate basic validation - await Task.CompletedTask; - return (true, "Location valid"); - } - - /// - /// Gets purchase count for player - /// - private async Task GetPurchaseCountAsync(int playerId, int kingdomId) - { - return await _context.PurchaseLogs - .Where(p => p.PlayerId == playerId && p.KingdomId == kingdomId) - .CountAsync(); - } - - /// - /// Assesses chargeback risk based on spending patterns - /// - private async Task AssessChargebackRiskAsync(int playerId, int kingdomId) - { - var recentPurchases = await _context.PurchaseLogs - .Where(p => p.PlayerId == playerId && p.KingdomId == kingdomId) - .Where(p => p.PurchaseDate >= DateTime.UtcNow.AddDays(-30)) - .CountAsync(); - - // High frequency purchases within short time = higher risk - return recentPurchases > 10; - } - - /// - /// Calculates combat effectiveness score - /// - private double CalculateCombatEffectiveness(Core.Models.Player player) - { - var totalCombats = player.AttackWins + player.AttackLosses + player.DefenseWins + player.DefenseLosses; - if (totalCombats == 0) return 0; - - var winRate = (double)(player.AttackWins + player.DefenseWins) / totalCombats; - var activityBonus = Math.Min(1.0, totalCombats / 100.0); // Bonus for more combat experience - - return (winRate * 100) * (1 + activityBonus); - } - - /// - /// Gets player power rank in kingdom - /// - private async Task GetPlayerPowerRankAsync(int playerId, int kingdomId) - { - var higherPowerCount = await _context.Players - .Where(p => p.KingdomId == kingdomId && p.IsActive) - .Where(p => p.Power > _context.Players.First(x => x.Id == playerId).Power) - .CountAsync(); - - return higherPowerCount + 1; - } - - /// - /// Gets retention status based on activity - /// - private string GetRetentionStatus(double daysSinceJoin, double daysSinceLastActivity) - { - if (daysSinceLastActivity <= 1) return "Highly Active"; - if (daysSinceLastActivity <= 3) return "Active"; - if (daysSinceLastActivity <= 7) return "Moderate"; - if (daysSinceLastActivity <= 14) return "At Risk"; - if (daysSinceLastActivity <= 30) return "Churning"; - return "Churned"; - } - - /// - /// Calculates engagement score - /// - private double CalculateEngagementScore(Core.Models.Player player) - { - var baseScore = 50.0; - - // Castle progression - baseScore += player.CastleLevel * 2; - - // Combat activity - var totalCombats = player.AttackWins + player.AttackLosses + player.DefenseWins + player.DefenseLosses; - baseScore += Math.Min(50, totalCombats * 0.5); - - // VIP engagement - baseScore += player.VipTier * 5; - - // Recent activity bonus - if (player.LastActivity.HasValue) - { - var daysSinceActivity = (DateTime.UtcNow - player.LastActivity.Value).TotalDays; - if (daysSinceActivity <= 1) baseScore += 20; - else if (daysSinceActivity <= 7) baseScore += 10; - } - - return Math.Min(100, baseScore); - } - - /// - /// Gets monetization tier classification - /// - private string GetMonetizationTier(decimal totalSpent) - { - if (totalSpent >= 1000) return "Whale"; - if (totalSpent >= 100) return "Dolphin"; - if (totalSpent >= 10) return "Minnow"; - if (totalSpent > 0) return "Paying"; - return "Free"; - } - - /// - /// Calculates progression rate - /// - private double CalculateProgressionRate(Core.Models.Player player, double daysSinceJoin) - { - if (daysSinceJoin <= 0) return 0; - return player.CastleLevel / daysSinceJoin; // Levels per day - } - - /// - /// Gets social engagement score - /// - private async Task GetSocialEngagementScoreAsync(int playerId, int kingdomId) - { - // In production, this would analyze alliance participation, chat activity, etc. - var isInAlliance = await _context.Players - .Where(p => p.Id == playerId && p.KingdomId == kingdomId) - .Select(p => p.AllianceId) - .FirstOrDefaultAsync() != null; - - return isInAlliance ? 75.0 : 25.0; // Base social score - } - - /// - /// Calculates churn risk score - /// - private string CalculateChurnRisk(Core.Models.Player player, double daysSinceLastActivity) - { - if (daysSinceLastActivity <= 1) return "Very Low"; - if (daysSinceLastActivity <= 3) return "Low"; - if (daysSinceLastActivity <= 7) return "Medium"; - if (daysSinceLastActivity <= 14) return "High"; - return "Very High"; + return recommendations; } #endregion