diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Alliance/Alliance.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Alliance/Alliance.cs new file mode 100644 index 0000000..e6a2c67 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Alliance/Alliance.cs @@ -0,0 +1,535 @@ +/* + * File: ShadowedRealms.Core/Models/Alliance/Alliance.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Core Alliance entity representing player organizations with territory, research, and coalition systems. Handles alliance hierarchy, progression, and KvK participation while preserving alliance independence. + * Last Edit Notes: Initial creation with 5-tier hierarchy, research trees, territory system, and coalition mechanics for KvK events + */ + +using ShadowedRealms.Core.Models.Kingdom; +using ShadowedRealms.Core.Models.Player; +using System.ComponentModel.DataAnnotations; + +namespace ShadowedRealms.Core.Models.Alliance +{ + public class Alliance + { + public int Id { get; set; } + + [Required] + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } = string.Empty; + + [Required] + [StringLength(5, MinimumLength = 2)] + public string Tag { get; set; } = string.Empty; + + [Required] + public int KingdomId { get; set; } + + [Required] + public int LeaderId { get; set; } + + [Required] + [Range(1, 25)] + public int Level { get; set; } = 1; + + [Required] + [Range(0, long.MaxValue)] + public long Power { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long ExperiencePoints { get; set; } = 0; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public bool IsActive { get; set; } = true; + + [Required] + [Range(10, 200)] + public int MaxMembers { get; set; } = 50; + + // Alliance Description and Settings + [StringLength(1000)] + public string? Description { get; set; } + + [StringLength(200)] + public string? WelcomeMessage { get; set; } + + public bool IsOpenToJoin { get; set; } = true; + + public bool RequireApproval { get; set; } = true; + + [Range(1, 30)] + public int MinCastleLevelToJoin { get; set; } = 1; + + [Range(0, long.MaxValue)] + public long MinPowerToJoin { get; set; } = 0; + + // Alliance Territory System + [Range(-1000, 1000)] + public int FortressX { get; set; } = 0; + + [Range(-1000, 1000)] + public int FortressY { get; set; } = 0; + + [Range(1, 10)] + public int FortressLevel { get; set; } = 1; + + [Range(0, 20)] + public int TowerCount { get; set; } = 0; + + [Range(0, 15)] + public int ResourceBuildingCount { get; set; } = 0; + + public bool HasTerritory { get; set; } = false; + + // Research Trees - Military Branch + [Range(0, 50)] + public int MilitaryAttackResearch { get; set; } = 0; + + [Range(0, 50)] + public int MilitaryDefenseResearch { get; set; } = 0; + + [Range(0, 50)] + public int MilitaryHealthResearch { get; set; } = 0; + + [Range(0, 50)] + public int MilitarySpeedResearch { get; set; } = 0; + + [Range(0, 50)] + public int MilitaryCapacityResearch { get; set; } = 0; + + // Research Trees - Economic Branch + [Range(0, 50)] + public int EconomicGatheringResearch { get; set; } = 0; + + [Range(0, 50)] + public int EconomicStorageResearch { get; set; } = 0; + + [Range(0, 50)] + public int EconomicTradeResearch { get; set; } = 0; + + [Range(0, 50)] + public int EconomicProductionResearch { get; set; } = 0; + + [Range(0, 50)] + public int EconomicTaxationResearch { get; set; } = 0; + + // Research Trees - Technology Branch + [Range(0, 50)] + public int TechnologyConstructionResearch { get; set; } = 0; + + [Range(0, 50)] + public int TechnologyUpgradeResearch { get; set; } = 0; + + [Range(0, 50)] + public int TechnologyTrainingResearch { get; set; } = 0; + + [Range(0, 50)] + public int TechnologyResearchResearch { get; set; } = 0; + + [Range(0, 50)] + public int TechnologyHealingResearch { get; set; } = 0; + + // Alliance Statistics + [Range(0, long.MaxValue)] + public long TotalKills { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long TotalDeaths { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long ResourcesGathered { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long ResourcesShared { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int KvKParticipations { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int KvKWins { get; set; } = 0; + + public DateTime? LastKvKParticipation { get; set; } + + // Coalition System for KvK + public int? CurrentCoalitionId { get; set; } + + public bool IsEligibleForKvK { get; set; } = true; + + public bool AcceptsCoalitionInvites { get; set; } = true; + + [Range(0, 100)] + public int CoalitionReputationScore { get; set; } = 50; // 0-100 scale + + // Alliance Treasury + [Range(0, long.MaxValue)] + public long TreasuryFood { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long TreasuryWood { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long TreasuryIron { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long TreasurySilver { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long TreasuryMithril { get; set; } = 0; + + // Navigation Properties + public virtual Kingdom.Kingdom Kingdom { get; set; } = null!; + + public virtual Player.Player Leader { get; set; } = null!; + + public virtual ICollection Members { get; set; } = new List(); + + public virtual ICollection Roles { get; set; } = new List(); + + public virtual ICollection PendingInvitations { get; set; } = new List(); + + // Computed Properties + public int ActiveMemberCount => Members?.Count(m => m.IsActive) ?? 0; + + public int InactiveMemberCount => Members?.Count(m => !m.IsActive) ?? 0; + + public double AverageMemberPower => ActiveMemberCount > 0 ? (double)Power / ActiveMemberCount : 0; + + public int AverageCastleLevel => ActiveMemberCount > 0 ? (int)Members.Where(m => m.IsActive).Average(m => m.CastleLevel) : 0; + + public long ExperienceToNextLevel => CalculateExperienceForLevel(Level + 1) - ExperiencePoints; + + public bool CanLevelUp => Level < 25 && ExperiencePoints >= CalculateExperienceForLevel(Level + 1); + + public int MaxMembersForLevel => CalculateMaxMembersForLevel(); + + public double KvKWinRate => KvKParticipations > 0 ? (double)KvKWins / KvKParticipations * 100 : 0; + + public AllianceHealthStatus HealthStatus => GetAllianceHealthStatus(); + + public long TotalTreasuryValue => TreasuryFood + TreasuryWood + TreasuryIron + TreasurySilver + TreasuryMithril; + + public bool HasFullTerritoryControl => HasTerritory && FortressLevel >= 5 && TowerCount >= 8 && ResourceBuildingCount >= 10; + + // Research bonus calculations + public double MilitaryAttackBonus => MilitaryAttackResearch * 0.5; // 0.5% per level + + public double MilitaryDefenseBonus => MilitaryDefenseResearch * 0.5; + + public double MilitaryHealthBonus => MilitaryHealthResearch * 0.5; + + public double MilitarySpeedBonus => MilitarySpeedResearch * 0.3; + + public double MilitaryCapacityBonus => MilitaryCapacityResearch * 1.0; + + public double EconomicGatheringBonus => EconomicGatheringResearch * 0.7; + + public double EconomicStorageBonus => EconomicStorageResearch * 1.0; + + public double EconomicTradeBonus => EconomicTradeResearch * 0.4; + + public double EconomicProductionBonus => EconomicProductionResearch * 0.6; + + public double TechnologyConstructionBonus => TechnologyConstructionResearch * 0.8; + + public double TechnologyUpgradeBonus => TechnologyUpgradeResearch * 0.6; + + public double TechnologyTrainingBonus => TechnologyTrainingResearch * 0.9; + + public double TechnologyHealingBonus => TechnologyHealingResearch * 1.2; + + // Business Logic Methods + private long CalculateExperienceForLevel(int level) + { + // Exponential XP requirements: Level * Level * 1000 + return level * level * 1000L; + } + + private int CalculateMaxMembersForLevel() + { + // Base 50 + 5 per level above 1 + return 50 + (Level - 1) * 5; + } + + public bool CanAcceptNewMember() + { + return IsActive && IsOpenToJoin && ActiveMemberCount < Math.Min(MaxMembers, MaxMembersForLevel); + } + + public bool MeetsMemberRequirements(Player.Player player) + { + if (player == null || !player.IsActive) return false; + if (player.CastleLevel < MinCastleLevelToJoin) return false; + if (player.Power < MinPowerToJoin) return false; + if (player.KingdomId != KingdomId) return false; + + return true; + } + + public void AddExperience(long xp) + { + ExperiencePoints += xp; + + // Auto-level up if possible + while (CanLevelUp && Level < 25) + { + Level++; + MaxMembers = MaxMembersForLevel; + } + } + + public void UpdatePower() + { + Power = Members?.Where(m => m.IsActive).Sum(m => m.Power) ?? 0; + } + + public void UpdateStatistics() + { + if (Members?.Any() != true) return; + + var activeMembers = Members.Where(m => m.IsActive); + + TotalKills = activeMembers.Sum(m => m.TroopsKilled); + TotalDeaths = activeMembers.Sum(m => m.TroopsLost); + ResourcesGathered = activeMembers.Sum(m => m.ResourcesGathered); + + UpdatePower(); + } + + public AllianceHealthStatus GetAllianceHealthStatus() + { + if (!IsActive) return AllianceHealthStatus.Disbanded; + + var memberCount = ActiveMemberCount; + var averagePower = AverageMemberPower; + var activityRatio = memberCount > 0 ? (double)ActiveMemberCount / (ActiveMemberCount + InactiveMemberCount) : 0; + + if (memberCount < 5 || activityRatio < 0.3) return AllianceHealthStatus.Critical; + if (memberCount < 15 || activityRatio < 0.5 || averagePower < 100000) return AllianceHealthStatus.Struggling; + if (memberCount < 25 || activityRatio < 0.7 || averagePower < 500000) return AllianceHealthStatus.Stable; + if (memberCount < 40 || activityRatio < 0.85 || averagePower < 1000000) return AllianceHealthStatus.Strong; + + return AllianceHealthStatus.Dominant; + } + + public bool CanFormCoalition(Alliance otherAlliance) + { + if (otherAlliance == null || !otherAlliance.IsActive || !IsActive) return false; + if (otherAlliance.KingdomId != KingdomId) return false; + if (!IsEligibleForKvK || !otherAlliance.IsEligibleForKvK) return false; + if (!AcceptsCoalitionInvites || !otherAlliance.AcceptsCoalitionInvites) return false; + + // Reputation threshold for coalition formation + var combinedReputation = (CoalitionReputationScore + otherAlliance.CoalitionReputationScore) / 2; + return combinedReputation >= 30; // Minimum 30% combined reputation + } + + public double CalculateResearchBonus(ResearchType researchType, ResearchCategory category) + { + return (researchType, category) switch + { + (ResearchType.Military, ResearchCategory.Attack) => MilitaryAttackBonus, + (ResearchType.Military, ResearchCategory.Defense) => MilitaryDefenseBonus, + (ResearchType.Military, ResearchCategory.Health) => MilitaryHealthBonus, + (ResearchType.Military, ResearchCategory.Speed) => MilitarySpeedBonus, + (ResearchType.Military, ResearchCategory.Capacity) => MilitaryCapacityBonus, + (ResearchType.Economic, ResearchCategory.Gathering) => EconomicGatheringBonus, + (ResearchType.Economic, ResearchCategory.Storage) => EconomicStorageBonus, + (ResearchType.Economic, ResearchCategory.Trade) => EconomicTradeBonus, + (ResearchType.Economic, ResearchCategory.Production) => EconomicProductionBonus, + (ResearchType.Technology, ResearchCategory.Construction) => TechnologyConstructionBonus, + (ResearchType.Technology, ResearchCategory.Upgrade) => TechnologyUpgradeBonus, + (ResearchType.Technology, ResearchCategory.Training) => TechnologyTrainingBonus, + (ResearchType.Technology, ResearchCategory.Healing) => TechnologyHealingBonus, + _ => 0 + }; + } + + public bool CanResearchTechnology(ResearchType type, ResearchCategory category, int currentLevel) + { + if (currentLevel >= 50) return false; // Max research level + + var requiredResources = CalculateResearchCost(type, category, currentLevel + 1); + return CanAffordResources(requiredResources.Food, requiredResources.Wood, + requiredResources.Iron, requiredResources.Silver, requiredResources.Mithril); + } + + public (long Food, long Wood, long Iron, long Silver, long Mithril) CalculateResearchCost(ResearchType type, ResearchCategory category, int level) + { + var baseCost = level * level * 10000L; // Exponential cost scaling + var multiplier = type switch + { + ResearchType.Military => 1.2, + ResearchType.Economic => 1.0, + ResearchType.Technology => 1.5, + _ => 1.0 + }; + + var totalCost = (long)(baseCost * multiplier); + + // Distribute cost across resources based on research type + return type switch + { + ResearchType.Military => (totalCost / 2, totalCost / 4, totalCost, totalCost / 8, totalCost / 16), + ResearchType.Economic => (totalCost, totalCost / 2, totalCost / 4, totalCost / 2, totalCost / 8), + ResearchType.Technology => (totalCost / 4, totalCost / 2, totalCost / 2, totalCost, totalCost / 4), + _ => (0, 0, 0, 0, 0) + }; + } + + public bool CanAffordResources(long food, long wood, long iron, long silver, long mithril) + { + return TreasuryFood >= food && TreasuryWood >= wood && TreasuryIron >= iron && + TreasurySilver >= silver && TreasuryMithril >= mithril; + } + + public void ConsumeResources(long food, long wood, long iron, long silver, long mithril) + { + if (!CanAffordResources(food, wood, iron, silver, mithril)) + throw new InvalidOperationException("Alliance treasury has insufficient resources"); + + TreasuryFood = Math.Max(0, TreasuryFood - food); + TreasuryWood = Math.Max(0, TreasuryWood - wood); + TreasuryIron = Math.Max(0, TreasuryIron - iron); + TreasurySilver = Math.Max(0, TreasurySilver - silver); + TreasuryMithril = Math.Max(0, TreasuryMithril - mithril); + } + + public void AddToTreasury(long food, long wood, long iron, long silver, long mithril) + { + TreasuryFood += food; + TreasuryWood += wood; + TreasuryIron += iron; + TreasurySilver += silver; + TreasuryMithril += mithril; + } + + public int GetTerritoryUpgradeCost(TerritoryUpgradeType upgradeType) + { + return upgradeType switch + { + TerritoryUpgradeType.Fortress => FortressLevel * 1000000, + TerritoryUpgradeType.Tower => (TowerCount + 1) * 500000, + TerritoryUpgradeType.ResourceBuilding => (ResourceBuildingCount + 1) * 300000, + _ => 0 + }; + } + + public List ValidateAllianceState() + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(Name) || Name.Length < 3) + errors.Add("Alliance name must be at least 3 characters long"); + + if (string.IsNullOrWhiteSpace(Tag) || Tag.Length < 2 || Tag.Length > 5) + errors.Add("Alliance tag must be between 2 and 5 characters"); + + if (Level < 1 || Level > 25) + errors.Add("Alliance level must be between 1 and 25"); + + if (MaxMembers < 10 || MaxMembers > MaxMembersForLevel) + errors.Add($"Max members must be between 10 and {MaxMembersForLevel} for level {Level}"); + + if (ActiveMemberCount > MaxMembers) + errors.Add("Alliance has more active members than maximum allowed"); + + return errors; + } + + public override string ToString() + { + return $"Alliance [{Tag}] {Name} (Level {Level}, {ActiveMemberCount}/{MaxMembers} members, Power: {Power:N0})"; + } + } + + // Supporting Classes + public class AllianceRole + { + public int Id { get; set; } + public int AllianceId { get; set; } + public int PlayerId { get; set; } + public AllianceRank Rank { get; set; } + public DateTime AssignedAt { get; set; } = DateTime.UtcNow; + public int AssignedById { get; set; } + + public virtual Alliance Alliance { get; set; } = null!; + public virtual Player.Player Player { get; set; } = null!; + public virtual Player.Player AssignedBy { get; set; } = null!; + } + + public class AllianceInvitation + { + public int Id { get; set; } + public int AllianceId { get; set; } + public int PlayerId { get; set; } + public int InvitedById { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddDays(7); + public bool IsAccepted { get; set; } = false; + public bool IsRejected { get; set; } = false; + + public virtual Alliance Alliance { get; set; } = null!; + public virtual Player.Player Player { get; set; } = null!; + public virtual Player.Player InvitedBy { get; set; } = null!; + + public bool IsExpired => DateTime.UtcNow > ExpiresAt; + public bool IsPending => !IsAccepted && !IsRejected && !IsExpired; + } + + // Supporting Enums + public enum AllianceRank + { + Member = 1, + Officer = 2, + CoLeader = 3, + DeputyLeader = 4, + Leader = 5 + } + + public enum AllianceHealthStatus + { + Disbanded = 0, + Critical = 1, + Struggling = 2, + Stable = 3, + Strong = 4, + Dominant = 5 + } + + public enum ResearchType + { + Military = 1, + Economic = 2, + Technology = 3 + } + + public enum ResearchCategory + { + Attack = 1, + Defense = 2, + Health = 3, + Speed = 4, + Capacity = 5, + Gathering = 6, + Storage = 7, + Trade = 8, + Production = 9, + Construction = 10, + Upgrade = 11, + Training = 12, + Healing = 13 + } + + public enum TerritoryUpgradeType + { + Fortress = 1, + Tower = 2, + ResourceBuilding = 3 + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Combat/CombatLog.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Combat/CombatLog.cs new file mode 100644 index 0000000..24873a8 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Combat/CombatLog.cs @@ -0,0 +1,536 @@ +/* + * File: ShadowedRealms.Core/Models/Combat/CombatLog.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Combat logging system for tracking all battle activities including field interception, castle sieges, and KvK events. Provides comprehensive audit trail for combat resolution and analytics. + * Last Edit Notes: Initial creation with field interception support, troop casualties, resource transfers, and dragon skill integration + */ + +using ShadowedRealms.Core.Models.Kingdom; +using ShadowedRealms.Core.Models.Player; +using ShadowedRealms.Core.Models.Alliance; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace ShadowedRealms.Core.Models.Combat +{ + public class CombatLog + { + public int Id { get; set; } + + [Required] + public int AttackerPlayerId { get; set; } + + [Required] + public int DefenderPlayerId { get; set; } + + [Required] + public int KingdomId { get; set; } + + public int? AttackerAllianceId { get; set; } + + public int? DefenderAllianceId { get; set; } + + [Required] + public CombatType CombatType { get; set; } + + [Required] + public CombatResult Result { get; set; } + + [Required] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + // Battle Location and Context + [Required] + [Range(-1000, 1000)] + public int BattleX { get; set; } + + [Required] + [Range(-1000, 1000)] + public int BattleY { get; set; } + + public bool WasFieldInterception { get; set; } = false; + + public int? InterceptorPlayerId { get; set; } + + public InterceptionType? InterceptionType { get; set; } + + // March Information + [Required] + public DateTime MarchStartTime { get; set; } + + [Required] + public DateTime MarchArrivalTime { get; set; } + + [Required] + [Range(0, int.MaxValue)] + public int MarchDurationSeconds { get; set; } + + public bool UsedSpeedBoosts { get; set; } = false; + + [Range(0, 10)] + public int SpeedBoostLevel { get; set; } = 0; + + // Attacker Army Composition (before battle) + [Required] + [Range(0, long.MaxValue)] + public long AttackerInfantryBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerCavalryBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerBowmenBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerSiegeBefore { get; set; } + + [Range(1, 13)] + public int AttackerHighestTroopTier { get; set; } = 1; + + // Attacker Army Composition (after battle) + [Required] + [Range(0, long.MaxValue)] + public long AttackerInfantryAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerCavalryAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerBowmenAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerSiegeAfter { get; set; } + + // Defender Army Composition (before battle) + [Required] + [Range(0, long.MaxValue)] + public long DefenderInfantryBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderCavalryBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderBowmenBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderSiegeBefore { get; set; } + + [Range(1, 13)] + public int DefenderHighestTroopTier { get; set; } = 1; + + // Defender Army Composition (after battle) + [Required] + [Range(0, long.MaxValue)] + public long DefenderInfantryAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderCavalryAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderBowmenAfter { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderSiegeAfter { get; set; } + + // Combat Statistics + [Required] + [Range(0, long.MaxValue)] + public long AttackerPowerBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderPowerBefore { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerCasualties { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderCasualties { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long AttackerWoundedTroops { get; set; } + + [Required] + [Range(0, long.MaxValue)] + public long DefenderWoundedTroops { get; set; } + + // Dragon System Integration + public bool AttackerHadDragon { get; set; } = false; + + public bool DefenderHadDragon { get; set; } = false; + + [StringLength(100)] + public string? AttackerDragonSkillsUsed { get; set; } + + [StringLength(100)] + public string? DefenderDragonSkillsUsed { get; set; } + + [Range(0, 100)] + public double AttackerDragonBonus { get; set; } = 0; + + [Range(0, 100)] + public double DefenderDragonBonus { get; set; } = 0; + + // Resource Transfer + [Required] + [Range(0, long.MaxValue)] + public long FoodTransferred { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long WoodTransferred { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long IronTransferred { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SilverTransferred { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long MithrilTransferred { get; set; } = 0; + + // Alliance Research Bonuses Applied + [Range(0, 100)] + public double AttackerAllianceAttackBonus { get; set; } = 0; + + [Range(0, 100)] + public double AttackerAllianceDefenseBonus { get; set; } = 0; + + [Range(0, 100)] + public double AttackerAllianceHealthBonus { get; set; } = 0; + + [Range(0, 100)] + public double DefenderAllianceAttackBonus { get; set; } = 0; + + [Range(0, 100)] + public double DefenderAllianceDefenseBonus { get; set; } = 0; + + [Range(0, 100)] + public double DefenderAllianceHealthBonus { get; set; } = 0; + + // KvK Context + public bool WasKvKBattle { get; set; } = false; + + public int? KvKEventId { get; set; } + + public int? CoalitionId { get; set; } + + public bool WasInForestZone { get; set; } = false; + + [Range(0, 100)] + public double ForestSpeedReduction { get; set; } = 0; + + // Battle Report Data (JSON storage for detailed breakdown) + [StringLength(4000)] + public string? DetailedBattleReport { get; set; } + + [StringLength(1000)] + public string? BattleNotes { get; set; } + + // Navigation Properties + public virtual Player.Player AttackerPlayer { get; set; } = null!; + + public virtual Player.Player DefenderPlayer { get; set; } = null!; + + public virtual Kingdom.Kingdom Kingdom { get; set; } = null!; + + public virtual Alliance.Alliance? AttackerAlliance { get; set; } + + public virtual Alliance.Alliance? DefenderAlliance { get; set; } + + public virtual Player.Player? InterceptorPlayer { get; set; } + + // Computed Properties + public long AttackerTotalTroopsBefore => AttackerInfantryBefore + AttackerCavalryBefore + AttackerBowmenBefore + AttackerSiegeBefore; + + public long AttackerTotalTroopsAfter => AttackerInfantryAfter + AttackerCavalryAfter + AttackerBowmenAfter + AttackerSiegeAfter; + + public long DefenderTotalTroopsBefore => DefenderInfantryBefore + DefenderCavalryBefore + DefenderBowmenBefore + DefenderSiegeBefore; + + public long DefenderTotalTroopsAfter => DefenderInfantryAfter + DefenderCavalryAfter + DefenderBowmenAfter + DefenderSiegeAfter; + + public double AttackerCasualtyRate => AttackerTotalTroopsBefore > 0 ? (double)AttackerCasualties / AttackerTotalTroopsBefore * 100 : 0; + + public double DefenderCasualtyRate => DefenderTotalTroopsBefore > 0 ? (double)DefenderCasualties / DefenderTotalTroopsBefore * 100 : 0; + + public long TotalResourcesTransferred => FoodTransferred + WoodTransferred + IronTransferred + SilverTransferred + MithrilTransferred; + + public bool WasDecisiveVictory => Result != CombatResult.Draw && (AttackerCasualtyRate < 10 || DefenderCasualtyRate < 10); + + public bool WasMajorBattle => AttackerTotalTroopsBefore + DefenderTotalTroopsBefore > 100000; + + public TimeSpan ActualMarchDuration => MarchArrivalTime - MarchStartTime; + + public double SpeedBoostEffectiveness => MarchDurationSeconds > 0 ? (double)ActualMarchDuration.TotalSeconds / MarchDurationSeconds : 1.0; + + // Business Logic Methods + public CombatEffectivenessRating GetCombatEffectiveness(bool forAttacker = true) + { + var casualties = forAttacker ? AttackerCasualties : DefenderCasualties; + var totalTroops = forAttacker ? AttackerTotalTroopsBefore : DefenderTotalTroopsBefore; + var won = forAttacker ? + (Result == CombatResult.AttackerVictory) : + (Result == CombatResult.DefenderVictory); + + if (totalTroops == 0) return CombatEffectivenessRating.NoEngagement; + + var casualtyRate = (double)casualties / totalTroops * 100; + + if (won) + { + return casualtyRate switch + { + <= 5 => CombatEffectivenessRating.Overwhelming, + <= 15 => CombatEffectivenessRating.Decisive, + <= 30 => CombatEffectivenessRating.Clear, + <= 50 => CombatEffectivenessRating.Narrow, + _ => CombatEffectivenessRating.Pyrrhic + }; + } + else + { + return casualtyRate switch + { + <= 10 => CombatEffectivenessRating.ValiantDefense, + <= 25 => CombatEffectivenessRating.StubbornResistance, + <= 50 => CombatEffectivenessRating.FoughtHard, + <= 75 => CombatEffectivenessRating.Overwhelmed, + _ => CombatEffectivenessRating.Crushed + }; + } + } + + public TroopComposition GetAttackerArmyBefore() + { + return new TroopComposition + { + Infantry = AttackerInfantryBefore, + Cavalry = AttackerCavalryBefore, + Bowmen = AttackerBowmenBefore, + Siege = AttackerSiegeBefore, + HighestTier = AttackerHighestTroopTier + }; + } + + public TroopComposition GetDefenderArmyBefore() + { + return new TroopComposition + { + Infantry = DefenderInfantryBefore, + Cavalry = DefenderCavalryBefore, + Bowmen = DefenderBowmenBefore, + Siege = DefenderSiegeBefore, + HighestTier = DefenderHighestTroopTier + }; + } + + public CombatSummary GenerateCombatSummary() + { + return new CombatSummary + { + BattleType = CombatType, + Result = Result, + AttackerName = AttackerPlayer?.Name ?? "Unknown", + DefenderName = DefenderPlayer?.Name ?? "Unknown", + AttackerAlliance = AttackerAlliance?.Tag, + DefenderAlliance = DefenderAlliance?.Tag, + AttackerCasualties = AttackerCasualties, + DefenderCasualties = DefenderCasualties, + ResourcesLooted = TotalResourcesTransferred, + WasFieldInterception = WasFieldInterception, + BattleEffectiveness = GetCombatEffectiveness(Result == CombatResult.AttackerVictory), + Timestamp = Timestamp + }; + } + + public bool InvolvesAlliances() + { + return AttackerAllianceId.HasValue || DefenderAllianceId.HasValue; + } + + public bool WasCrossAllianceBattle() + { + return AttackerAllianceId.HasValue && DefenderAllianceId.HasValue && + AttackerAllianceId != DefenderAllianceId; + } + + public void SetDetailedBattleReport(DetailedBattleReport report) + { + DetailedBattleReport = JsonSerializer.Serialize(report); + } + + public DetailedBattleReport? GetDetailedBattleReport() + { + if (string.IsNullOrWhiteSpace(DetailedBattleReport)) return null; + + try + { + return JsonSerializer.Deserialize(DetailedBattleReport); + } + catch + { + return null; + } + } + + public List ValidateCombatLog() + { + var errors = new List(); + + if (AttackerPlayerId == DefenderPlayerId) + errors.Add("Attacker and defender cannot be the same player"); + + if (MarchStartTime >= MarchArrivalTime) + errors.Add("March start time must be before arrival time"); + + if (Timestamp < MarchStartTime) + errors.Add("Battle timestamp cannot be before march start time"); + + if (AttackerTotalTroopsBefore == 0) + errors.Add("Attacker must have troops to engage in combat"); + + if (AttackerCasualties > AttackerTotalTroopsBefore) + errors.Add("Attacker casualties cannot exceed total troops"); + + if (DefenderCasualties > DefenderTotalTroopsBefore) + errors.Add("Defender casualties cannot exceed total troops"); + + if (WasFieldInterception && !InterceptorPlayerId.HasValue) + errors.Add("Field interception requires an interceptor player"); + + if (WasKvKBattle && !KvKEventId.HasValue) + errors.Add("KvK battle requires a KvK event ID"); + + return errors; + } + + public override string ToString() + { + var attackerName = AttackerPlayer?.Name ?? $"Player {AttackerPlayerId}"; + var defenderName = DefenderPlayer?.Name ?? $"Player {DefenderPlayerId}"; + var resultText = Result switch + { + CombatResult.AttackerVictory => $"{attackerName} defeated {defenderName}", + CombatResult.DefenderVictory => $"{defenderName} defended against {attackerName}", + CombatResult.Draw => $"{attackerName} vs {defenderName} ended in a draw", + _ => "Unknown battle result" + }; + + var interceptionText = WasFieldInterception ? " (Field Interception)" : ""; + var kvkText = WasKvKBattle ? " [KvK]" : ""; + + return $"{CombatType}: {resultText}{interceptionText}{kvkText}"; + } + } + + // Supporting Classes and Data Structures + public class TroopComposition + { + public long Infantry { get; set; } + public long Cavalry { get; set; } + public long Bowmen { get; set; } + public long Siege { get; set; } + public int HighestTier { get; set; } + + public long Total => Infantry + Cavalry + Bowmen + Siege; + } + + public class CombatSummary + { + public CombatType BattleType { get; set; } + public CombatResult Result { get; set; } + public string AttackerName { get; set; } = string.Empty; + public string DefenderName { get; set; } = string.Empty; + public string? AttackerAlliance { get; set; } + public string? DefenderAlliance { get; set; } + public long AttackerCasualties { get; set; } + public long DefenderCasualties { get; set; } + public long ResourcesLooted { get; set; } + public bool WasFieldInterception { get; set; } + public CombatEffectivenessRating BattleEffectiveness { get; set; } + public DateTime Timestamp { get; set; } + } + + public class DetailedBattleReport + { + public List Rounds { get; set; } = new List(); + public Dictionary AttackerBonuses { get; set; } = new Dictionary(); + public Dictionary DefenderBonuses { get; set; } = new Dictionary(); + public string? SpecialEvents { get; set; } + public TimeSpan BattleDuration { get; set; } + } + + public class BattleRound + { + public int RoundNumber { get; set; } + public long AttackerDamageDealt { get; set; } + public long DefenderDamageDealt { get; set; } + public long AttackerCasualties { get; set; } + public long DefenderCasualties { get; set; } + public string? SpecialEffects { get; set; } + } + + // Supporting Enums + public enum CombatType + { + LightningRaid = 1, // Unrestricted speed, small force + StandardAttack = 2, // Normal restrictions + CastleSiege = 3, // Fully restricted, large force + ResourceGathering = 4, // Attacking resource nodes + AllianceWarfare = 5, // Alliance vs Alliance + KvKBattle = 6, // Kingdom vs Kingdom + DefensiveBattle = 7, // Defending castle/territory + FieldInterception = 8 // Meeting attackers before they reach target + } + + public enum CombatResult + { + AttackerVictory = 1, + DefenderVictory = 2, + Draw = 3, + AttackerWithdrew = 4, + DefenderFled = 5 + } + + public enum InterceptionType + { + AutomaticDefense = 1, // System-triggered interception + ManualIntercept = 2, // Player-initiated interception + AllianceSupport = 3, // Alliance member intercepting + CoalitionDefense = 4 // Coalition member intercepting (KvK) + } + + public enum CombatEffectivenessRating + { + NoEngagement = 0, + Crushed = 1, + Overwhelmed = 2, + FoughtHard = 3, + StubbornResistance = 4, + ValiantDefense = 5, + Pyrrhic = 6, + Narrow = 7, + Clear = 8, + Decisive = 9, + Overwhelming = 10 + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Kingdom/Kingdom.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Kingdom/Kingdom.cs new file mode 100644 index 0000000..0da0fdb --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Kingdom/Kingdom.cs @@ -0,0 +1,245 @@ +/* + * File: ShadowedRealms.Core/Models/Kingdom/Kingdom.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Core Kingdom entity representing a game server/realm. Handles kingdom-level statistics, population management, and serves as the root entity for all kingdom-scoped data. + * Last Edit Notes: Initial creation with population management, merger system support, and KvK participation tracking + */ + +using ShadowedRealms.Core.Models.Alliance; +using ShadowedRealms.Core.Models.Player; +using System.ComponentModel.DataAnnotations; +using System.Numerics; + +namespace ShadowedRealms.Core.Models.Kingdom +{ + public class Kingdom + { + public int Id { get; set; } + + [Required] + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } = string.Empty; + + [Required] + public int Number { get; set; } + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public bool IsActive { get; set; } = true; + + [Required] + [Range(100, 3000)] + public int MaxPopulation { get; set; } = 1500; + + [Required] + [Range(0, int.MaxValue)] + public int CurrentPopulation { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long TotalPower { get; set; } = 0; + + public DateTime? LastKvKDate { get; set; } + + [Range(0, int.MaxValue)] + public int KvKWins { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int KvKLosses { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int KvKDraws { get; set; } = 0; + + public bool IsEligibleForKvK { get; set; } = true; + + public bool AcceptingNewPlayers { get; set; } = true; + + public DateTime? MergerAvailableDate { get; set; } + + public bool IsEligibleForMerger { get; set; } = false; + + [StringLength(500)] + public string? Description { get; set; } + + [StringLength(200)] + public string? WelcomeMessage { get; set; } + + // Kingdom settings + public bool AutoKickInactivePlayers { get; set; } = true; + + [Range(1, 90)] + public int InactivityKickDays { get; set; } = 30; + + public bool EnableTaxSystem { get; set; } = false; + + [Range(0.0, 0.1)] + public decimal TaxRate { get; set; } = 0.04m; // 4% default + + // Navigation properties + public virtual ICollection Players { get; set; } = new List(); + + public virtual ICollection Alliances { get; set; } = new List(); + + // Computed properties + public int ActivePlayerCount => Players?.Count(p => p.IsActive) ?? 0; + + public int ActiveAllianceCount => Alliances?.Count(a => a.IsActive && a.Members.Any(m => m.IsActive)) ?? 0; + + public double PopulationPercentage => MaxPopulation > 0 ? (double)CurrentPopulation / MaxPopulation * 100 : 0; + + public bool IsNearCapacity => PopulationPercentage >= 90; + + public bool IsOverCapacity => CurrentPopulation > MaxPopulation; + + public int KvKTotalGames => KvKWins + KvKLosses + KvKDraws; + + public double KvKWinRate => KvKTotalGames > 0 ? (double)KvKWins / KvKTotalGames * 100 : 0; + + public bool CanParticipateInKvK => IsActive && IsEligibleForKvK && ActiveAllianceCount >= 3; + + public int DaysUntilMergerEligible + { + get + { + if (MergerAvailableDate == null) return -1; + var days = (MergerAvailableDate.Value - DateTime.UtcNow).Days; + return Math.Max(0, days); + } + } + + // Kingdom health metrics + public KingdomHealthStatus GetHealthStatus() + { + if (!IsActive) return KingdomHealthStatus.Inactive; + + var activePlayers = ActivePlayerCount; + var populationPercentage = PopulationPercentage; + + if (activePlayers < 100) return KingdomHealthStatus.Critical; + if (activePlayers < 300 || populationPercentage < 20) return KingdomHealthStatus.Poor; + if (activePlayers < 600 || populationPercentage < 40) return KingdomHealthStatus.Fair; + if (activePlayers < 900 || populationPercentage < 60) return KingdomHealthStatus.Good; + + return KingdomHealthStatus.Excellent; + } + + // Kingdom merger eligibility check + public bool CanMergeWith(Kingdom otherKingdom) + { + if (otherKingdom == null || !otherKingdom.IsActive || !IsActive) + return false; + + if (!IsEligibleForMerger || !otherKingdom.IsEligibleForMerger) + return false; + + var combinedPopulation = CurrentPopulation + otherKingdom.CurrentPopulation; + var targetMaxPopulation = Math.Max(MaxPopulation, otherKingdom.MaxPopulation); + + return combinedPopulation <= targetMaxPopulation * 1.1; // Allow 10% over capacity temporarily + } + + // Resource tax calculation + public long CalculateTaxAmount(long resourceAmount) + { + if (!EnableTaxSystem || TaxRate <= 0) + return 0; + + return (long)(resourceAmount * (decimal)TaxRate); + } + + // Kingdom progression tracking + public void UpdatePopulation() + { + CurrentPopulation = Players?.Count(p => p.IsActive) ?? 0; + TotalPower = Players?.Where(p => p.IsActive).Sum(p => p.Power) ?? 0; + + // Update merger eligibility based on population health + var healthStatus = GetHealthStatus(); + IsEligibleForMerger = healthStatus == KingdomHealthStatus.Critical || + healthStatus == KingdomHealthStatus.Poor; + + // Set merger available date if becoming eligible + if (IsEligibleForMerger && MergerAvailableDate == null) + { + MergerAvailableDate = DateTime.UtcNow.AddDays(30); // 30-day waiting period + } + else if (!IsEligibleForMerger) + { + MergerAvailableDate = null; + } + } + + // KvK record management + public void RecordKvKResult(KvKResult result) + { + switch (result) + { + case KvKResult.Win: + KvKWins++; + break; + case KvKResult.Loss: + KvKLosses++; + break; + case KvKResult.Draw: + KvKDraws++; + break; + } + + LastKvKDate = DateTime.UtcNow; + + // Update KvK eligibility based on recent performance and kingdom health + var healthStatus = GetHealthStatus(); + IsEligibleForKvK = healthStatus != KingdomHealthStatus.Critical && + healthStatus != KingdomHealthStatus.Inactive; + } + + // Validation methods + public List ValidateKingdomSettings() + { + var errors = new List(); + + if (MaxPopulation < 100) + errors.Add("Maximum population must be at least 100 players"); + + if (MaxPopulation > 3000) + errors.Add("Maximum population cannot exceed 3000 players"); + + if (TaxRate < 0 || TaxRate > 0.1m) + errors.Add("Tax rate must be between 0% and 10%"); + + if (InactivityKickDays < 1 || InactivityKickDays > 90) + errors.Add("Inactivity kick period must be between 1 and 90 days"); + + if (string.IsNullOrWhiteSpace(Name) || Name.Length < 3) + errors.Add("Kingdom name must be at least 3 characters long"); + + return errors; + } + + public override string ToString() + { + return $"Kingdom {Number}: {Name} ({CurrentPopulation}/{MaxPopulation} players)"; + } + } + + // Supporting enums + public enum KingdomHealthStatus + { + Inactive = 0, + Critical = 1, + Poor = 2, + Fair = 3, + Good = 4, + Excellent = 5 + } + + public enum KvKResult + { + Win = 1, + Loss = 2, + Draw = 3 + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs new file mode 100644 index 0000000..08f43b4 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/Player.cs @@ -0,0 +1,516 @@ +/* + * File: ShadowedRealms.Core/Models/Player/Player.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Core Player entity representing a game player with castle, resources, troops, and progression systems. Handles player statistics, alliance membership, and combat capabilities. + * Last Edit Notes: Initial creation with castle progression, resource management, troop systems, and alliance integration + */ + +using ShadowedRealms.Core.Models.Alliance; +using ShadowedRealms.Core.Models.Kingdom; +using System.ComponentModel.DataAnnotations; + +namespace ShadowedRealms.Core.Models.Player +{ + public class Player + { + public int Id { get; set; } + + [Required] + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } = string.Empty; + + [Required] + public int KingdomId { get; set; } + + public int? AllianceId { get; set; } + + [Required] + [Range(1, 44)] + public int CastleLevel { get; set; } = 1; + + [Required] + [Range(0, long.MaxValue)] + public long Power { get; set; } = 0; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime LastActiveAt { get; set; } = DateTime.UtcNow; + + [Required] + public bool IsActive { get; set; } = true; + + // Coordinates + [Required] + [Range(-1000, 1000)] + public int CoordinateX { get; set; } = 0; + + [Required] + [Range(-1000, 1000)] + public int CoordinateY { get; set; } = 0; + + // Resources + [Required] + [Range(0, long.MaxValue)] + public long Food { get; set; } = 50000; + + [Required] + [Range(0, long.MaxValue)] + public long Wood { get; set; } = 50000; + + [Required] + [Range(0, long.MaxValue)] + public long Iron { get; set; } = 50000; + + [Required] + [Range(0, long.MaxValue)] + public long Silver { get; set; } = 10000; + + [Required] + [Range(0, long.MaxValue)] + public long Mithril { get; set; } = 1000; + + // Protected storage (cannot be raided) + [Required] + [Range(0, long.MaxValue)] + public long FoodProtected { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long WoodProtected { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long IronProtected { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SilverProtected { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long MithrilProtected { get; set; } = 0; + + // Premium Currency + [Required] + [Range(0, int.MaxValue)] + public int Gold { get; set; } = 100; // Starting gold + + // Troops - Tier 1 through Tier 13 (T1-T13) + [Required] + [Range(0, long.MaxValue)] + public long InfantryT1 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT1 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT1 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT1 { get; set; } = 0; + + // Higher tier troops (T2-T13) - will be unlocked based on castle level + [Required] + [Range(0, long.MaxValue)] + public long InfantryT2 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT2 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT2 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT2 { get; set; } = 0; + + // T3 Troops + [Required] + [Range(0, long.MaxValue)] + public long InfantryT3 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT3 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT3 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT3 { get; set; } = 0; + + // T4-T10 Troops (Launch maximum is T10 at Castle 30) + [Required] + [Range(0, long.MaxValue)] + public long InfantryT4 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT4 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT4 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT4 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long InfantryT5 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT5 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT5 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT5 { get; set; } = 0; + + // T6-T10 (continuing pattern for space efficiency) + [Required] + [Range(0, long.MaxValue)] + public long InfantryT10 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT10 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT10 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT10 { get; set; } = 0; + + // Post-launch expansion troops (T11-T13) + [Required] + [Range(0, long.MaxValue)] + public long InfantryT11 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT11 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT11 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT11 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long InfantryT12 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT12 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT12 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT12 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long InfantryT13 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long CavalryT13 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long BowmenT13 { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long SiegeT13 { get; set; } = 0; + + // Player Statistics + [Required] + [Range(0, long.MaxValue)] + public long AttacksWon { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long AttacksLost { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long DefensesWon { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long DefensesLost { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long TroopsKilled { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long TroopsLost { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long ResourcesGathered { get; set; } = 0; + + [Required] + [Range(0, long.MaxValue)] + public long ResourcesRaided { get; set; } = 0; + + // Player Settings + public bool AcceptAllianceInvites { get; set; } = true; + public bool ShowOnlineStatus { get; set; } = true; + public bool EnablePushNotifications { get; set; } = true; + public bool AutoUseSpeedBoosts { get; set; } = false; + + [StringLength(500)] + public string? Biography { get; set; } + + // VIP and Premium Status + [Range(0, 15)] + public int VipLevel { get; set; } = 0; + + public DateTime? VipExpiryDate { get; set; } + + public bool HasActiveDragon { get; set; } = false; + + public DateTime? DragonExpiryDate { get; set; } + + // Navigation properties + public virtual Kingdom.Kingdom Kingdom { get; set; } = null!; + + public virtual Alliance.Alliance? Alliance { get; set; } + + // Computed properties + public bool IsVipActive => VipExpiryDate.HasValue && VipExpiryDate.Value > DateTime.UtcNow; + + public bool IsDragonActive => HasActiveDragon && DragonExpiryDate.HasValue && DragonExpiryDate.Value > DateTime.UtcNow; + + public int DaysInactive => (DateTime.UtcNow - LastActiveAt).Days; + + public bool IsInactive => DaysInactive > 7; + + public bool IsCriticallyInactive => DaysInactive > 30; + + public long TotalTroops => GetTotalTroopsByType(TroopType.All); + + public long TotalResources => Food + Wood + Iron + Silver + Mithril; + + public long TotalProtectedResources => FoodProtected + WoodProtected + IronProtected + SilverProtected + MithrilProtected; + + public long TotalRaidableResources => TotalResources - TotalProtectedResources; + + public int MaxTroopTier => GetMaxUnlockedTroopTier(); + + public PlayerRankStatus RankStatus => GetPlayerRankStatus(); + + public double WinRate + { + get + { + var totalBattles = AttacksWon + AttacksLost + DefensesWon + DefensesLost; + if (totalBattles == 0) return 0; + var totalWins = AttacksWon + DefensesWon; + return (double)totalWins / totalBattles * 100; + } + } + + public double KillDeathRatio => TroopsLost > 0 ? (double)TroopsKilled / TroopsLost : TroopsKilled; + + // Business logic methods + public long GetTotalTroopsByType(TroopType troopType) + { + return troopType switch + { + TroopType.Infantry => InfantryT1 + InfantryT2 + InfantryT3 + InfantryT4 + InfantryT5 + InfantryT10 + InfantryT11 + InfantryT12 + InfantryT13, + TroopType.Cavalry => CavalryT1 + CavalryT2 + CavalryT3 + CavalryT4 + CavalryT5 + CavalryT10 + CavalryT11 + CavalryT12 + CavalryT13, + TroopType.Bowmen => BowmenT1 + BowmenT2 + BowmenT3 + BowmenT4 + BowmenT5 + BowmenT10 + BowmenT11 + BowmenT12 + BowmenT13, + TroopType.Siege => SiegeT1 + SiegeT2 + SiegeT3 + SiegeT4 + SiegeT5 + SiegeT10 + SiegeT11 + SiegeT12 + SiegeT13, + TroopType.All => GetTotalTroopsByType(TroopType.Infantry) + GetTotalTroopsByType(TroopType.Cavalry) + GetTotalTroopsByType(TroopType.Bowmen) + GetTotalTroopsByType(TroopType.Siege), + _ => 0 + }; + } + + public int GetMaxUnlockedTroopTier() + { + // Based on castle level - launch parameters (T10 max at Castle 30) + return CastleLevel switch + { + >= 44 => 13, // Post-launch: T13 at Castle 44 + >= 40 => 12, // Post-launch: T12 at Castle 40 + >= 35 => 11, // Post-launch: T11 at Castle 35 + >= 30 => 10, // Launch: T10 at Castle 30 + >= 27 => 9, + >= 24 => 8, + >= 21 => 7, + >= 18 => 6, + >= 15 => 5, + >= 12 => 4, + >= 9 => 3, + >= 6 => 2, + _ => 1 // T1 troops available from start + }; + } + + public bool CanTrainTroopTier(int tier) + { + return tier <= GetMaxUnlockedTroopTier(); + } + + public long CalculateStorageCapacity() + { + // Base storage increases with castle level + var baseCapacity = CastleLevel * 100000L; // 100k per castle level + var vipBonus = IsVipActive ? (long)(baseCapacity * (VipLevel * 0.05)) : 0; // 5% per VIP level + return baseCapacity + vipBonus; + } + + public long CalculateProtectedStorage() + { + // Protected storage is 25% of total capacity, increased by VIP + var baseProtected = CalculateStorageCapacity() * 0.25; + var vipBonus = IsVipActive ? baseProtected * (VipLevel * 0.02) : 0; // 2% per VIP level + return (long)(baseProtected + vipBonus); + } + + public bool CanAffordResources(long food, long wood, long iron, long silver, long mithril) + { + return Food >= food && Wood >= wood && Iron >= iron && Silver >= silver && Mithril >= mithril; + } + + public void ConsumeResources(long food, long wood, long iron, long silver, long mithril) + { + if (!CanAffordResources(food, wood, iron, silver, mithril)) + throw new InvalidOperationException("Insufficient resources"); + + Food = Math.Max(0, Food - food); + Wood = Math.Max(0, Wood - wood); + Iron = Math.Max(0, Iron - iron); + Silver = Math.Max(0, Silver - silver); + Mithril = Math.Max(0, Mithril - mithril); + } + + public void AddResources(long food, long wood, long iron, long silver, long mithril) + { + var capacity = CalculateStorageCapacity(); + + Food = Math.Min(capacity, Food + food); + Wood = Math.Min(capacity, Wood + wood); + Iron = Math.Min(capacity, Iron + iron); + Silver = Math.Min(capacity, Silver + silver); + Mithril = Math.Min(capacity, Mithril + mithril); + } + + public PlayerRankStatus GetPlayerRankStatus() + { + return CastleLevel switch + { + >= 40 => PlayerRankStatus.Legendary, + >= 35 => PlayerRankStatus.Epic, + >= 30 => PlayerRankStatus.Elite, + >= 25 => PlayerRankStatus.Veteran, + >= 20 => PlayerRankStatus.Advanced, + >= 15 => PlayerRankStatus.Intermediate, + >= 10 => PlayerRankStatus.Novice, + _ => PlayerRankStatus.Beginner + }; + } + + public void UpdateLastActive() + { + LastActiveAt = DateTime.UtcNow; + if (!IsActive) IsActive = true; + } + + public bool CanJoinAlliance(Alliance.Alliance alliance) + { + if (alliance == null || !alliance.IsActive) return false; + if (AllianceId.HasValue) return false; // Already in alliance + if (alliance.KingdomId != KingdomId) return false; // Different kingdom + if (alliance.Members.Count >= alliance.MaxMembers) return false; // Alliance full + + return true; + } + + public void CalculatePower() + { + // Power calculation based on troops, castle level, and resources + var troopPower = TotalTroops * 10; // Base 10 power per troop + var castlePower = CastleLevel * CastleLevel * 1000L; // Exponential castle power + var resourcePower = TotalResources / 1000; // 1 power per 1k resources + + Power = troopPower + castlePower + resourcePower; + } + + public List ValidatePlayerState() + { + var errors = new List(); + + if (CastleLevel < 1 || CastleLevel > 44) + errors.Add("Castle level must be between 1 and 44"); + + if (CoordinateX < -1000 || CoordinateX > 1000 || CoordinateY < -1000 || CoordinateY > 1000) + errors.Add("Coordinates must be within map boundaries (-1000 to 1000)"); + + if (string.IsNullOrWhiteSpace(Name) || Name.Length < 3) + errors.Add("Player name must be at least 3 characters long"); + + var storageCapacity = CalculateStorageCapacity(); + if (TotalResources > storageCapacity * 1.1) // Allow 10% overflow + errors.Add("Resource storage significantly exceeded"); + + return errors; + } + + public override string ToString() + { + return $"Player {Name} (Castle {CastleLevel}, Power: {Power:N0})"; + } + } + + // Supporting enums + public enum TroopType + { + Infantry = 1, + Cavalry = 2, + Bowmen = 3, + Siege = 4, + All = 99 + } + + public enum PlayerRankStatus + { + Beginner = 1, + Novice = 2, + Intermediate = 3, + Advanced = 4, + Veteran = 5, + Elite = 6, + Epic = 7, + Legendary = 8 + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/VipRewards.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/VipRewards.cs new file mode 100644 index 0000000..d6c7676 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Player/VipRewards.cs @@ -0,0 +1,458 @@ +/* + * File: ShadowedRealms.Core/Models/Player/VipRewards.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: VIP rewards and milestone system inspired by King of Avalon's undisclosed spending tier rewards. Tracks spending milestones, secret tiers, and monthly/yearly recognition rewards for high-value customers. + * Last Edit Notes: Initial creation with secret tier system, milestone rewards, and whale retention mechanics + */ + +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace ShadowedRealms.Core.Models.Player +{ + public class VipRewards + { + public int Id { get; set; } + + [Required] + public int PlayerId { get; set; } + + [Required] + public int KingdomId { get; set; } + + // VIP Points System (separate from spending to allow flexibility) + [Required] + [Range(0, int.MaxValue)] + public int CurrentVipPoints { get; set; } = 0; + + [Required] + [Range(0, int.MaxValue)] + public int LifetimeVipPoints { get; set; } = 0; + + [Required] + [Range(0, int.MaxValue)] + public int MonthlyVipPoints { get; set; } = 0; + + [Required] + [Range(0, int.MaxValue)] + public int YearlyVipPoints { get; set; } = 0; + + // Secret Spending Tiers (undisclosed like King of Avalon) + [Range(0, 15)] + public int SecretTierLevel { get; set; } = 0; // 0-15 undisclosed tiers + + [Range(0, 15)] + public int HighestSecretTierReached { get; set; } = 0; + + public DateTime? LastSecretTierUnlock { get; set; } + + // Monthly Recognition System + public bool EligibleForMonthlyRewards { get; set; } = false; + + [Range(0, 12)] + public int MonthlyRewardsEarned { get; set; } = 0; // How many months they've earned rewards + + public DateTime? LastMonthlyRewardDate { get; set; } + + [Range(0, double.MaxValue)] + public decimal MonthlySpendingForRewards { get; set; } = 0; + + // Yearly Recognition System + public bool EligibleForYearlyRewards { get; set; } = false; + + [Range(0, 10)] + public int YearlyRewardsEarned { get; set; } = 0; // How many years they've earned rewards + + public DateTime? LastYearlyRewardDate { get; set; } + + [Range(0, double.MaxValue)] + public decimal YearlySpendingForRewards { get; set; } = 0; + + // Milestone Tracking + public DateTime? LastMilestoneReached { get; set; } + + [StringLength(100)] + public string? LastMilestoneType { get; set; } + + [Range(0, int.MaxValue)] + public int TotalMilestonesReached { get; set; } = 0; + + // Special Recognition Status + public bool IsVipAmbassador { get; set; } = false; // Top 1% of spenders + + public bool IsFoundingSupporter { get; set; } = false; // Early high spenders + + public bool IsLoyalSupporter { get; set; } = false; // Consistent monthly spenders + + public bool IsLegendarySupporter { get; set; } = false; // Extreme lifetime spenders + + // Streak Tracking + [Range(0, int.MaxValue)] + public int ConsecutiveMonthsSpending { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int LongestSpendingStreak { get; set; } = 0; + + public DateTime? CurrentStreakStartDate { get; set; } + + // Reward History (JSON storage for complex data) + [StringLength(4000)] + public string? RewardHistory { get; set; } + + [StringLength(2000)] + public string? SpecialRecognitions { get; set; } + + // Dates for tracking + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime LastUpdated { get; set; } = DateTime.UtcNow; + + public DateTime MonthlyResetDate { get; set; } = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1).AddMonths(1); + + public DateTime YearlyResetDate { get; set; } = new DateTime(DateTime.UtcNow.Year + 1, 1, 1); + + // Navigation Properties + public virtual Player Player { get; set; } = null!; + + public virtual ICollection MilestonesReached { get; set; } = new List(); + + public virtual ICollection RewardsClaimed { get; set; } = new List(); + + // Computed Properties + public int DaysUntilMonthlyReset => (MonthlyResetDate - DateTime.UtcNow).Days; + + public int DaysUntilYearlyReset => (YearlyResetDate - DateTime.UtcNow).Days; + + public bool IsCurrentMonthSpender => MonthlyVipPoints > 0; + + public bool IsCurrentYearSpender => YearlyVipPoints > 0; + + public VipTierStatus CurrentTierStatus => GetVipTierStatus(); + + public SecretTierRewardLevel SecretRewardLevel => GetSecretTierRewardLevel(); + + public bool HasEarnedMonthlyReward => MonthlyRewardsEarned > 0; + + public bool HasEarnedYearlyReward => YearlyRewardsEarned > 0; + + public double AverageMonthlySpending => MonthlyRewardsEarned > 0 ? (double)YearlySpendingForRewards / MonthlyRewardsEarned : 0; + + // Business Logic Methods + public void AddVipPoints(int points, decimal spendingAmount) + { + CurrentVipPoints += points; + LifetimeVipPoints += points; + MonthlyVipPoints += points; + YearlyVipPoints += points; + + MonthlySpendingForRewards += spendingAmount; + YearlySpendingForRewards += spendingAmount; + + CheckForMilestones(); + UpdateSecretTier(); + UpdateSpecialStatus(); + LastUpdated = DateTime.UtcNow; + } + + private void CheckForMilestones() + { + var newMilestones = new List(); + + // Check VIP point milestones (every 10,000 points) + var pointMilestone = (LifetimeVipPoints / 10000) * 10000; + if (pointMilestone > 0 && (LastMilestoneReached == null || + LifetimeVipPoints - (int)(LastMilestoneReached?.Subtract(DateTime.MinValue).TotalDays ?? 0) >= 10000)) + { + newMilestones.Add($"VIP_POINTS_{pointMilestone}"); + } + + // Check monthly spending milestones + if (MonthlySpendingForRewards >= 100 && !EligibleForMonthlyRewards) + { + EligibleForMonthlyRewards = true; + newMilestones.Add("MONTHLY_ELIGIBLE"); + } + + // Check yearly spending milestones + if (YearlySpendingForRewards >= 500 && !EligibleForYearlyRewards) + { + EligibleForYearlyRewards = true; + newMilestones.Add("YEARLY_ELIGIBLE"); + } + + // Update streak + UpdateSpendingStreak(); + + if (newMilestones.Any()) + { + TotalMilestonesReached += newMilestones.Count; + LastMilestoneReached = DateTime.UtcNow; + LastMilestoneType = string.Join(",", newMilestones); + } + } + + private void UpdateSecretTier() + { + // Secret tier thresholds (undisclosed to players like KoA) + var secretTierThresholds = new decimal[] + { + 0, // Tier 0 + 50, // Tier 1 - $50 lifetime + 150, // Tier 2 - $150 lifetime + 300, // Tier 3 - $300 lifetime + 500, // Tier 4 - $500 lifetime + 750, // Tier 5 - $750 lifetime + 1000, // Tier 6 - $1,000 lifetime + 1500, // Tier 7 - $1,500 lifetime + 2500, // Tier 8 - $2,500 lifetime + 4000, // Tier 9 - $4,000 lifetime + 6000, // Tier 10 - $6,000 lifetime + 10000, // Tier 11 - $10,000 lifetime + 15000, // Tier 12 - $15,000 lifetime + 25000, // Tier 13 - $25,000 lifetime + 50000, // Tier 14 - $50,000 lifetime + 100000 // Tier 15 - $100,000 lifetime (theoretical max) + }; + + var newTier = 0; + for (int i = secretTierThresholds.Length - 1; i >= 0; i--) + { + if (YearlySpendingForRewards >= secretTierThresholds[i]) + { + newTier = i; + break; + } + } + + if (newTier > SecretTierLevel) + { + SecretTierLevel = newTier; + if (newTier > HighestSecretTierReached) + { + HighestSecretTierReached = newTier; + LastSecretTierUnlock = DateTime.UtcNow; + } + } + } + + private void UpdateSpecialStatus() + { + // VIP Ambassador - Top tier current year spenders + IsVipAmbassador = SecretTierLevel >= 10 && IsCurrentYearSpender; + + // Founding Supporter - High spenders in first 6 months + var gameAge = DateTime.UtcNow.Subtract(new DateTime(2025, 1, 1)); // Assuming game launch + IsFoundingSupporter = gameAge.TotalDays <= 180 && HighestSecretTierReached >= 6; + + // Loyal Supporter - 6+ consecutive months of spending + IsLoyalSupporter = ConsecutiveMonthsSpending >= 6; + + // Legendary Supporter - Lifetime tier 12+ + IsLegendarySupporter = HighestSecretTierReached >= 12; + } + + private void UpdateSpendingStreak() + { + if (IsCurrentMonthSpender) + { + if (CurrentStreakStartDate == null) + { + CurrentStreakStartDate = DateTime.UtcNow; + ConsecutiveMonthsSpending = 1; + } + else + { + var monthsSinceStart = (DateTime.UtcNow.Year - CurrentStreakStartDate.Value.Year) * 12 + + DateTime.UtcNow.Month - CurrentStreakStartDate.Value.Month; + ConsecutiveMonthsSpending = monthsSinceStart + 1; + } + + if (ConsecutiveMonthsSpending > LongestSpendingStreak) + { + LongestSpendingStreak = ConsecutiveMonthsSpending; + } + } + } + + public VipTierStatus GetVipTierStatus() + { + return SecretTierLevel switch + { + >= 12 => VipTierStatus.Legendary, + >= 10 => VipTierStatus.Ambassador, + >= 8 => VipTierStatus.Elite, + >= 6 => VipTierStatus.Premium, + >= 4 => VipTierStatus.Advanced, + >= 2 => VipTierStatus.Standard, + >= 1 => VipTierStatus.Bronze, + _ => VipTierStatus.None + }; + } + + public SecretTierRewardLevel GetSecretTierRewardLevel() + { + // Different reward levels based on secret tier (like KoA's system) + return SecretTierLevel switch + { + >= 13 => SecretTierRewardLevel.Mythical, // $25,000+ + >= 11 => SecretTierRewardLevel.Legendary, // $10,000+ + >= 9 => SecretTierRewardLevel.Epic, // $2,500+ + >= 7 => SecretTierRewardLevel.Rare, // $1,500+ + >= 5 => SecretTierRewardLevel.Uncommon, // $750+ + >= 3 => SecretTierRewardLevel.Common, // $300+ + >= 1 => SecretTierRewardLevel.Basic, // $50+ + _ => SecretTierRewardLevel.None + }; + } + + // Monthly reset (called by background service) + public void ResetMonthlyProgress() + { + MonthlyVipPoints = 0; + MonthlySpendingForRewards = 0; + EligibleForMonthlyRewards = false; + MonthlyResetDate = MonthlyResetDate.AddMonths(1); + + // Break spending streak if no spending this month + if (MonthlyVipPoints == 0) + { + ConsecutiveMonthsSpending = 0; + CurrentStreakStartDate = null; + } + + LastUpdated = DateTime.UtcNow; + } + + // Yearly reset (called by background service) + public void ResetYearlyProgress() + { + YearlyVipPoints = 0; + YearlySpendingForRewards = 0; + EligibleForYearlyRewards = false; + YearlyResetDate = YearlyResetDate.AddYears(1); + SecretTierLevel = 0; // Secret tiers reset yearly like KoA + + LastUpdated = DateTime.UtcNow; + } + + // Reward claiming + public bool ClaimMonthlyReward() + { + if (!EligibleForMonthlyRewards || LastMonthlyRewardDate?.Month == DateTime.UtcNow.Month) + return false; + + MonthlyRewardsEarned++; + LastMonthlyRewardDate = DateTime.UtcNow; + return true; + } + + public bool ClaimYearlyReward() + { + if (!EligibleForYearlyRewards || LastYearlyRewardDate?.Year == DateTime.UtcNow.Year) + return false; + + YearlyRewardsEarned++; + LastYearlyRewardDate = DateTime.UtcNow; + return true; + } + + public List GetAvailableRewards() + { + var rewards = new List(); + + // Monthly rewards + if (EligibleForMonthlyRewards && (LastMonthlyRewardDate?.Month != DateTime.UtcNow.Month)) + { + rewards.Add(new VipRewardTier + { + Type = "Monthly", + TierLevel = SecretTierLevel, + RewardLevel = GetSecretTierRewardLevel(), + CanClaim = true + }); + } + + // Yearly rewards + if (EligibleForYearlyRewards && (LastYearlyRewardDate?.Year != DateTime.UtcNow.Year)) + { + rewards.Add(new VipRewardTier + { + Type = "Yearly", + TierLevel = HighestSecretTierReached, + RewardLevel = GetSecretTierRewardLevel(), + CanClaim = true + }); + } + + return rewards; + } + + public override string ToString() + { + var tierStatus = CurrentTierStatus; + var rewardLevel = SecretRewardLevel; + return $"VIP {tierStatus} (Secret Tier {SecretTierLevel}) - {rewardLevel} Rewards"; + } + } + + // Supporting Classes + public class VipMilestone + { + public int Id { get; set; } + public int VipRewardsId { get; set; } + public string MilestoneType { get; set; } = string.Empty; + public string MilestoneDescription { get; set; } = string.Empty; + public DateTime ReachedAt { get; set; } + public bool RewardClaimed { get; set; } = false; + + public virtual VipRewards VipRewards { get; set; } = null!; + } + + public class VipReward + { + public int Id { get; set; } + public int VipRewardsId { get; set; } + public string RewardType { get; set; } = string.Empty; // "Monthly", "Yearly", "Milestone" + public SecretTierRewardLevel RewardLevel { get; set; } + public string RewardContents { get; set; } = string.Empty; + public DateTime EarnedAt { get; set; } + public DateTime? ClaimedAt { get; set; } + public bool IsClaimed { get; set; } = false; + + public virtual VipRewards VipRewards { get; set; } = null!; + } + + public class VipRewardTier + { + public string Type { get; set; } = string.Empty; + public int TierLevel { get; set; } + public SecretTierRewardLevel RewardLevel { get; set; } + public bool CanClaim { get; set; } + } + + // Supporting Enums + public enum VipTierStatus + { + None = 0, + Bronze = 1, + Standard = 2, + Advanced = 3, + Premium = 4, + Elite = 5, + Ambassador = 6, + Legendary = 7 + } + + public enum SecretTierRewardLevel + { + None = 0, + Basic = 1, // $50+ yearly + Common = 2, // $300+ yearly + Uncommon = 3, // $750+ yearly + Rare = 4, // $1,500+ yearly + Epic = 5, // $2,500+ yearly + Legendary = 6, // $10,000+ yearly + Mythical = 7 // $25,000+ yearly + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Purchase/PurchaseLog.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Purchase/PurchaseLog.cs new file mode 100644 index 0000000..3a86a37 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Core/Models/Purchase/PurchaseLog.cs @@ -0,0 +1,758 @@ +/* + * File: ShadowedRealms.Core/Models/Purchase/PurchaseLog.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Purchase tracking system for all monetization activities including IAP, gold spending, VIP subscriptions, and dragon purchases. Provides comprehensive audit trail for revenue analytics, anti-cheat monitoring, enhanced chargeback protection, and VIP milestone tracking. + * Last Edit Notes: Updated with enhanced customer segmentation (monthly/lifetime), comprehensive chargeback protection, and VIP milestone/rewards tracking system + */ + +using ShadowedRealms.Core.Models.Kingdom; +using ShadowedRealms.Core.Models.Player; +using ShadowedRealms.Core.Models.Alliance; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace ShadowedRealms.Core.Models.Purchase +{ + public class PurchaseLog + { + public int Id { get; set; } + + [Required] + public int PlayerId { get; set; } + + [Required] + public int KingdomId { get; set; } + + public int? AllianceId { get; set; } + + [Required] + [StringLength(100)] + public string ProductId { get; set; } = string.Empty; + + [Required] + [StringLength(200)] + public string ProductName { get; set; } = string.Empty; + + [Required] + public PurchaseType PurchaseType { get; set; } + + [Required] + public PaymentMethod PaymentMethod { get; set; } + + [Required] + [Range(0, double.MaxValue)] + public decimal Amount { get; set; } + + [Required] + [StringLength(10)] + public string Currency { get; set; } = "USD"; + + [Required] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + public DateTime? ProcessedAt { get; set; } + + [Required] + public PurchaseStatus Status { get; set; } = PurchaseStatus.Pending; + + // Enhanced Platform-specific transaction tracking for chargeback protection + [Required] + [StringLength(200)] + public string PlatformTransactionId { get; set; } = string.Empty; + + [StringLength(500)] + public string? PlatformReceiptData { get; set; } + + [Required] + [StringLength(20)] + public string Platform { get; set; } = string.Empty; // iOS, Android, PC + + [StringLength(100)] + public string? PlatformUserId { get; set; } + + [StringLength(50)] + public string? DeviceId { get; set; } + + [StringLength(100)] + public string? IpAddress { get; set; } + + [StringLength(200)] + public string? UserAgent { get; set; } + + // Enhanced Gold Economy Tracking + [Range(0, int.MaxValue)] + public int GoldPurchased { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int GoldSpent { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int GoldBalanceBefore { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int GoldBalanceAfter { get; set; } = 0; + + // VIP System Integration + public bool IsVipPurchase { get; set; } = false; + + [Range(0, 15)] + public int VipLevelBefore { get; set; } = 0; + + [Range(0, 15)] + public int VipLevelAfter { get; set; } = 0; + + [Range(0, 365)] + public int VipDaysAdded { get; set; } = 0; + + // VIP Milestone and Rewards Tracking (KoA-inspired system) + [Range(0, int.MaxValue)] + public int VipPointsEarned { get; set; } = 0; // Points earned from this purchase + + [Range(0, int.MaxValue)] + public int VipPointsBefore { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int VipPointsAfter { get; set; } = 0; + + public bool TriggeredVipMilestone { get; set; } = false; + + [StringLength(100)] + public string? VipMilestoneReached { get; set; } + + public bool IsEligibleForMonthlyReward { get; set; } = false; + + public bool IsEligibleForYearlyReward { get; set; } = false; + + // Secret tier tracking (undisclosed thresholds like KoA) + [Range(0, 15)] + public int SecretSpendingTierBefore { get; set; } = 0; + + [Range(0, 15)] + public int SecretSpendingTierAfter { get; set; } = 0; + + public bool UnlockedNewSecretTier { get; set; } = false; + + // Dragon System Integration + public bool IsDragonPurchase { get; set; } = false; + + [StringLength(50)] + public string? DragonType { get; set; } + + [Range(0, 30)] + public int DragonDaysAdded { get; set; } = 0; + + [StringLength(200)] + public string? DragonSkillsUnlocked { get; set; } + + // Speed Boost Tracking + public bool IsSpeedBoostPurchase { get; set; } = false; + + [Range(0, 1000)] + public int SpeedBoostQuantity { get; set; } = 0; + + [Range(1, 10)] + public int SpeedBoostLevel { get; set; } = 0; + + [Range(0, 168)] // Up to 1 week + public int SpeedBoostHours { get; set; } = 0; + + // Stealth System Integration + public bool IsStealthPurchase { get; set; } = false; + + [Range(0, 100)] + public int StealthUsesAdded { get; set; } = 0; + + [StringLength(50)] + public string? StealthType { get; set; } // "Premium", "Instant", "Advanced" + + // Resource Package Tracking + public bool IsResourcePackage { get; set; } = false; + + [Range(0, long.MaxValue)] + public long FoodReceived { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long WoodReceived { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long IronReceived { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long SilverReceived { get; set; } = 0; + + [Range(0, long.MaxValue)] + public long MithrilReceived { get; set; } = 0; + + // Construction and Training Boosts + public bool IsConstructionBoost { get; set; } = false; + + public bool IsTrainingBoost { get; set; } = false; + + public bool IsResearchBoost { get; set; } = false; + + [Range(0, 168)] + public int BoostHours { get; set; } = 0; + + [Range(0, 500)] + public int BoostPercentage { get; set; } = 0; + + // Anti-Pay-to-Win Tracking + [Range(0, 100)] + public double SkillAlternativeUsed { get; set; } = 0; // Percentage of skill-based alternative used + + public bool HasSkillBasedAlternative { get; set; } = false; + + [StringLength(200)] + public string? SkillAlternativeDescription { get; set; } + + [Range(0, double.MaxValue)] + public decimal TimeInvestmentHours { get; set; } = 0; // Time player could have invested instead + + // Bundle and Package Information + public bool IsBundle { get; set; } = false; + + [StringLength(1000)] + public string? BundleContents { get; set; } + + [Range(0, 100)] + public double BundleDiscountPercentage { get; set; } = 0; + + // Enhanced Refund and Chargeback Tracking + public bool WasRefunded { get; set; } = false; + + public DateTime? RefundedAt { get; set; } + + [Range(0, double.MaxValue)] + public decimal RefundAmount { get; set; } = 0; + + [StringLength(500)] + public string? RefundReason { get; set; } + + public bool WasChargeback { get; set; } = false; + + public DateTime? ChargebackAt { get; set; } + + [StringLength(500)] + public string? ChargebackReason { get; set; } + + public DateTime? ChargebackDisputeDeadline { get; set; } + + public bool ChargebackDisputed { get; set; } = false; + + public ChargebackDisputeStatus? DisputeStatus { get; set; } + + [Range(0, double.MaxValue)] + public decimal ChargebackFee { get; set; } = 0; + + // Enhanced Fraud Detection and Chargeback Prevention + [Range(0, 100)] + public int FraudScore { get; set; } = 0; + + public bool IsFraudSuspected { get; set; } = false; + + [StringLength(500)] + public string? FraudNotes { get; set; } + + public bool RequiresManualReview { get; set; } = false; + + public bool IsHighRiskTransaction { get; set; } = false; + + [Range(0, int.MaxValue)] + public int MinutesFromRegistration { get; set; } = 0; // Time since account creation + + [Range(0, int.MaxValue)] + public int PurchasesInLast24Hours { get; set; } = 0; + + [Range(0, double.MaxValue)] + public decimal SpendingInLast24Hours { get; set; } = 0; + + public bool IsVelocityFlagged { get; set; } = false; // Too many purchases too quickly + + public bool IsNewDevicePurchase { get; set; } = false; // First purchase from this device + + // Enhanced Customer Segmentation - Monthly AND Lifetime + [Range(0, double.MaxValue)] + public decimal MonthlySpendingBefore { get; set; } = 0; + + [Range(0, double.MaxValue)] + public decimal MonthlySpendingAfter { get; set; } = 0; + + [Range(0, double.MaxValue)] + public decimal LifetimeSpendingBefore { get; set; } = 0; + + [Range(0, double.MaxValue)] + public decimal LifetimeSpendingAfter { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int MonthlyPurchaseCount { get; set; } = 0; + + [Range(0, int.MaxValue)] + public int LifetimePurchaseCount { get; set; } = 0; + + // Detailed Purchase Data (JSON storage) + [StringLength(2000)] + public string? DetailedPurchaseData { get; set; } + + [StringLength(1000)] + public string? PurchaseNotes { get; set; } + + // Analytics and Conversion Tracking + [StringLength(100)] + public string? CampaignId { get; set; } + + [StringLength(100)] + public string? PromotionCode { get; set; } + + public bool IsFirstPurchase { get; set; } = false; + + public bool IsFirstMonthlyPurchase { get; set; } = false; + + [Range(0, int.MaxValue)] + public int DaysSinceLastPurchase { get; set; } = 0; + + // Navigation Properties + public virtual Player.Player Player { get; set; } = null!; + + public virtual Kingdom.Kingdom Kingdom { get; set; } = null!; + + public virtual Alliance.Alliance? Alliance { get; set; } + + // Computed Properties - Enhanced Customer Segmentation + public long TotalResourcesReceived => FoodReceived + WoodReceived + IronReceived + SilverReceived + MithrilReceived; + + public bool IsSuccessfulPurchase => Status == PurchaseStatus.Completed; + + public bool IsPendingOrFailed => Status == PurchaseStatus.Pending || Status == PurchaseStatus.Failed; + + public decimal NetAmount => Amount - RefundAmount - ChargebackFee; + + public bool IsHighValuePurchase => Amount >= 50m; // $50+ purchases + + public bool IsRecentPurchase => (DateTime.UtcNow - Timestamp).TotalHours <= 24; + + public PurchaseCategory Category => GetPurchaseCategory(); + + public decimal DollarPerHour => TimeInvestmentHours > 0 ? Amount / TimeInvestmentHours : 0; + + public bool PromotesSkillBasedPlay => HasSkillBasedAlternative && SkillAlternativeUsed > 50; + + public TimeSpan ProcessingTime => ProcessedAt.HasValue ? ProcessedAt.Value - Timestamp : TimeSpan.Zero; + + // Enhanced Customer Segmentation Properties + public MonthlyCustomerSegment MonthlySegment => GetMonthlyCustomerSegment(); + + public LifetimeCustomerSegment LifetimeSegment => GetLifetimeCustomerSegment(); + + public bool IsMonthlyWhale => MonthlySpendingAfter >= 500m; + + public bool IsLifetimeWhale => LifetimeSpendingAfter >= 2000m; + + public bool IsChargebackRisk => GetChargebackRisk() >= ChargebackRiskLevel.High; + + public decimal TotalFinancialImpact => NetAmount < 0 ? Math.Abs(NetAmount) + ChargebackFee : NetAmount; + + public bool UnlockedSecretTier => UnlockedNewSecretTier; + + public string SecretTierProgression => $"{SecretSpendingTierBefore}→{SecretSpendingTierAfter}"; + + // Business Logic Methods + public PurchaseCategory GetPurchaseCategory() + { + if (IsVipPurchase) return PurchaseCategory.Subscription; + if (IsDragonPurchase) return PurchaseCategory.Premium; + if (IsResourcePackage) return PurchaseCategory.Resources; + if (IsSpeedBoostPurchase || IsConstructionBoost || IsTrainingBoost || IsResearchBoost) return PurchaseCategory.TimeSkip; + if (IsStealthPurchase) return PurchaseCategory.Convenience; + if (GoldPurchased > 0) return PurchaseCategory.Currency; + + return PurchaseCategory.Other; + } + + // Enhanced Customer Segmentation + public MonthlyCustomerSegment GetMonthlyCustomerSegment() + { + return MonthlySpendingAfter switch + { + >= 500m => MonthlyCustomerSegment.MonthlyWhale, // $500+ this month + >= 200m => MonthlyCustomerSegment.MonthlyDolphin, // $200-499 this month + >= 50m => MonthlyCustomerSegment.MonthlyMinnow, // $50-199 this month + >= 10m => MonthlyCustomerSegment.MonthlySpender, // $10-49 this month + > 0m => MonthlyCustomerSegment.MonthlyOccasional, // $1-9 this month + _ => MonthlyCustomerSegment.MonthlyFree // $0 this month + }; + } + + public LifetimeCustomerSegment GetLifetimeCustomerSegment() + { + return LifetimeSpendingAfter switch + { + >= 2000m => LifetimeCustomerSegment.LifetimeMegaWhale, // $2000+ lifetime + >= 1000m => LifetimeCustomerSegment.LifetimeWhale, // $1000-1999 lifetime + >= 500m => LifetimeCustomerSegment.LifetimeDolphin, // $500-999 lifetime + >= 100m => LifetimeCustomerSegment.LifetimeMinnow, // $100-499 lifetime + >= 20m => LifetimeCustomerSegment.LifetimeSpender, // $20-99 lifetime + > 0m => LifetimeCustomerSegment.LifetimeOccasional, // $1-19 lifetime + _ => LifetimeCustomerSegment.LifetimeFree // $0 lifetime + }; + } + + // Enhanced Chargeback Risk Assessment + public ChargebackRiskLevel GetChargebackRisk() + { + var riskScore = 0; + + // High-risk indicators + if (MinutesFromRegistration < 60) riskScore += 30; // Purchase within 1 hour of registration + if (PurchasesInLast24Hours >= 5) riskScore += 25; // 5+ purchases in 24 hours + if (SpendingInLast24Hours >= 200m) riskScore += 20; // $200+ in 24 hours + if (IsNewDevicePurchase && Amount >= 100m) riskScore += 15; // Large purchase from new device + if (IsHighValuePurchase && IsFirstPurchase) riskScore += 15; // First purchase is high value + if (string.IsNullOrWhiteSpace(IpAddress)) riskScore += 10; // No IP tracking + + // Medium-risk indicators + if (MinutesFromRegistration < 1440) riskScore += 10; // Purchase within 24 hours of registration + if (PurchasesInLast24Hours >= 3) riskScore += 10; // 3+ purchases in 24 hours + if (Amount >= 50m && MonthlyPurchaseCount == 1) riskScore += 8; // High-value first monthly purchase + if (PaymentMethod == PaymentMethod.CreditCard) riskScore += 5; // Credit cards have higher chargeback rates + + // Low-risk indicators (reduce risk) + if (LifetimePurchaseCount >= 10) riskScore -= 10; // Established customer + if (MinutesFromRegistration >= 43200) riskScore -= 10; // Account older than 30 days + if (Player?.Alliance != null) riskScore -= 5; // Alliance member (more invested) + + riskScore = Math.Max(0, Math.Min(100, riskScore)); // Clamp to 0-100 + + return riskScore switch + { + >= 70 => ChargebackRiskLevel.Critical, + >= 50 => ChargebackRiskLevel.High, + >= 30 => ChargebackRiskLevel.Medium, + >= 15 => ChargebackRiskLevel.Low, + _ => ChargebackRiskLevel.Minimal + }; + } + + public PayToWinImpact AssessPayToWinImpact() + { + if (HasSkillBasedAlternative && SkillAlternativeUsed >= 80) + return PayToWinImpact.Negligible; + + if (IsVipPurchase || IsDragonPurchase) + { + return MonthlySpendingAfter switch + { + >= 500m => PayToWinImpact.High, + >= 200m => PayToWinImpact.Moderate, + >= 50m => PayToWinImpact.Low, + _ => PayToWinImpact.Minimal + }; + } + + if (IsResourcePackage || IsSpeedBoostPurchase) + return Amount >= 20m ? PayToWinImpact.Moderate : PayToWinImpact.Low; + + if (IsStealthPurchase) + return PayToWinImpact.Minimal; // Convenience only + + return PayToWinImpact.Negligible; + } + + // VIP Milestone Processing + public void ProcessVipMilestone(int newVipPoints, int newSecretTier) + { + VipPointsBefore = VipPointsAfter; + VipPointsAfter += newVipPoints; + VipPointsEarned = newVipPoints; + + SecretSpendingTierBefore = SecretSpendingTierAfter; + SecretSpendingTierAfter = newSecretTier; + + if (newSecretTier > SecretSpendingTierBefore) + { + UnlockedNewSecretTier = true; + TriggeredVipMilestone = true; + VipMilestoneReached = $"SECRET_TIER_{newSecretTier}"; + } + + // Check milestone eligibility + if (MonthlySpendingAfter >= 100m) + IsEligibleForMonthlyReward = true; + + if (LifetimeSpendingAfter >= 500m) + IsEligibleForYearlyReward = true; + } + + // Chargeback Management Methods + public void ProcessChargeback(string reason, decimal chargebackFee = 15m) + { + WasChargeback = true; + ChargebackAt = DateTime.UtcNow; + ChargebackReason = reason; + ChargebackFee = chargebackFee; + Status = PurchaseStatus.Chargeback; + IsFraudSuspected = true; + RequiresManualReview = true; + ChargebackDisputeDeadline = DateTime.UtcNow.AddDays(10); // 10 days to dispute + } + + public void DisputeChargeback() + { + if (!WasChargeback) + throw new InvalidOperationException("Cannot dispute a non-chargeback transaction"); + + if (DateTime.UtcNow > ChargebackDisputeDeadline) + throw new InvalidOperationException("Chargeback dispute deadline has passed"); + + ChargebackDisputed = true; + DisputeStatus = ChargebackDisputeStatus.UnderReview; + RequiresManualReview = true; + } + + public bool ShouldBlockFuturePurchases() + { + // Block purchases for high-risk patterns + if (WasChargeback && !ChargebackDisputed) return true; + if (GetChargebackRisk() >= ChargebackRiskLevel.Critical) return true; + if (FraudScore >= 90) return true; + if (PurchasesInLast24Hours >= 10) return true; // Extreme velocity + + return false; + } + + public double CalculateValuePerDollar() + { + if (Amount == 0) return 0; + + var valueScore = 0.0; + + // Assign value scores to different purchase types + if (IsVipPurchase) valueScore += VipDaysAdded * 10; + if (IsDragonPurchase) valueScore += DragonDaysAdded * 15; + if (IsResourcePackage) valueScore += (double)TotalResourcesReceived / 10000; + if (GoldPurchased > 0) valueScore += GoldPurchased * 0.1; + if (IsSpeedBoostPurchase) valueScore += SpeedBoostHours * 5; + + return valueScore / (double)Amount; + } + + public void SetDetailedPurchaseData(DetailedPurchaseData data) + { + DetailedPurchaseData = JsonSerializer.Serialize(data); + } + + public DetailedPurchaseData? GetDetailedPurchaseData() + { + if (string.IsNullOrWhiteSpace(DetailedPurchaseData)) return null; + + try + { + return JsonSerializer.Deserialize(DetailedPurchaseData); + } + catch + { + return null; + } + } + + public void ProcessRefund(decimal refundAmount, string reason) + { + if (refundAmount > Amount) + throw new InvalidOperationException("Refund amount cannot exceed purchase amount"); + + WasRefunded = true; + RefundedAt = DateTime.UtcNow; + RefundAmount = refundAmount; + RefundReason = reason; + Status = PurchaseStatus.Refunded; + } + + public List ValidatePurchaseLog() + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(ProductId)) + errors.Add("Product ID is required"); + + if (string.IsNullOrWhiteSpace(PlatformTransactionId)) + errors.Add("Platform transaction ID is required for chargeback protection"); + + if (Amount < 0) + errors.Add("Purchase amount cannot be negative"); + + if (RefundAmount > Amount) + errors.Add("Refund amount cannot exceed purchase amount"); + + if (IsVipPurchase && VipDaysAdded <= 0) + errors.Add("VIP purchase must add VIP days"); + + if (IsDragonPurchase && DragonDaysAdded <= 0) + errors.Add("Dragon purchase must add dragon days"); + + if (IsSpeedBoostPurchase && SpeedBoostQuantity <= 0) + errors.Add("Speed boost purchase must include boost quantity"); + + if (GoldBalanceAfter < GoldBalanceBefore - GoldSpent) + errors.Add("Gold balance calculation is incorrect"); + + if (LifetimeSpendingAfter < LifetimeSpendingBefore) + errors.Add("Lifetime spending cannot decrease"); + + if (MonthlySpendingAfter < MonthlySpendingBefore) + errors.Add("Monthly spending cannot decrease"); + + if (ProcessedAt.HasValue && ProcessedAt.Value < Timestamp) + errors.Add("Processed time cannot be before purchase time"); + + if (VipPointsAfter < VipPointsBefore) + errors.Add("VIP points cannot decrease from a purchase"); + + if (SecretSpendingTierAfter < SecretSpendingTierBefore) + errors.Add("Secret spending tier cannot decrease"); + + return errors; + } + + public override string ToString() + { + var playerName = Player?.Name ?? $"Player {PlayerId}"; + var statusText = Status switch + { + PurchaseStatus.Completed => "✓", + PurchaseStatus.Pending => "⏳", + PurchaseStatus.Failed => "✗", + PurchaseStatus.Refunded => "↩", + PurchaseStatus.Chargeback => "⚠", + _ => "?" + }; + + var segmentText = $"M:{MonthlySegment:F0}/L:{LifetimeSegment:F0}"; + var riskText = IsChargebackRisk ? " 🚨" : ""; + var vipText = UnlockedNewSecretTier ? " 🎁" : ""; + + return $"{statusText} {playerName}: {ProductName} - {Amount:C} ({segmentText}){riskText}{vipText}"; + } + } + + // Supporting Classes + public class DetailedPurchaseData + { + public Dictionary PlatformData { get; set; } = new Dictionary(); + public List ItemsReceived { get; set; } = new List(); + public Dictionary BonusMultipliers { get; set; } = new Dictionary(); + public string? SpecialOfferInfo { get; set; } + public DateTime? OfferExpiryDate { get; set; } + public List RequirementsMet { get; set; } = new List(); + public Dictionary RiskIndicators { get; set; } = new Dictionary(); + public Dictionary VipMilestoneData { get; set; } = new Dictionary(); + } + + // Enhanced Supporting Enums + public enum PurchaseType + { + InAppPurchase = 1, // Real money -> Gold/Items + GoldSpending = 2, // Gold -> Game items + Subscription = 3, // VIP/Premium subscriptions + ResourcePurchase = 4, // Direct resource purchases + BoostPurchase = 5, // Time skip and boost purchases + CosmeticPurchase = 6 // Appearance and cosmetic items + } + + public enum PaymentMethod + { + Unknown = 0, + ApplePay = 1, + GooglePlay = 2, + CreditCard = 3, + PayPal = 4, + BankTransfer = 5, + Cryptocurrency = 6, + GiftCard = 7, + PromotionalCredit = 8, + GoldBalance = 9 // Using in-game currency + } + + public enum PurchaseStatus + { + Pending = 1, + Completed = 2, + Failed = 3, + Cancelled = 4, + Refunded = 5, + Chargeback = 6, + UnderReview = 7, + Blocked = 8 // Blocked due to fraud risk + } + + public enum PurchaseCategory + { + Currency = 1, // Gold purchases + Resources = 2, // Food/Wood/Iron/Silver/Mithril + TimeSkip = 3, // Speed boosts, construction boosts + Premium = 4, // Dragons, special items + Subscription = 5, // VIP, monthly subscriptions + Convenience = 6, // Stealth, quality of life + Cosmetic = 7, // Appearance items + Other = 8 + } + + // Enhanced Customer Segmentation Enums + public enum MonthlyCustomerSegment + { + MonthlyFree = 1, // $0 this month + MonthlyOccasional = 2, // $1-9 this month + MonthlySpender = 3, // $10-49 this month + MonthlyMinnow = 4, // $50-199 this month + MonthlyDolphin = 5, // $200-499 this month + MonthlyWhale = 6 // $500+ this month + } + + public enum LifetimeCustomerSegment + { + LifetimeFree = 1, // $0 lifetime + LifetimeOccasional = 2, // $1-19 lifetime + LifetimeSpender = 3, // $20-99 lifetime + LifetimeMinnow = 4, // $100-499 lifetime + LifetimeDolphin = 5, // $500-999 lifetime + LifetimeWhale = 6, // $1000-1999 lifetime + LifetimeMegaWhale = 7 // $2000+ lifetime + } + + public enum ChargebackRiskLevel + { + Minimal = 1, // 0-14% risk score + Low = 2, // 15-29% risk score + Medium = 3, // 30-49% risk score + High = 4, // 50-69% risk score + Critical = 5 // 70-100% risk score + } + + public enum ChargebackDisputeStatus + { + UnderReview = 1, + Won = 2, // Dispute successful, chargeback reversed + Lost = 3, // Dispute failed, chargeback stands + PartialWin = 4, // Partial reversal + Expired = 5 // Dispute deadline passed + } + + public enum PayToWinImpact + { + Negligible = 1, // No competitive advantage + Minimal = 2, // Very slight advantage + Low = 3, // Minor advantage + Moderate = 4, // Noticeable advantage + High = 5, // Significant advantage + Extreme = 6 // Game-breaking advantage + } + + public enum PurchaseTrend + { + NewSpender = 1, + FrequentSpender = 2, // Multiple purchases per week + RegularSpender = 3, // Weekly purchases + OccasionalSpender = 4, // Monthly purchases + ReturnedSpender = 5, // Returned after 1-3 months + LongAbsentSpender = 6 // Returned after 3+ months + } +} \ No newline at end of file diff --git a/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Contexts/GameDbContext.cs b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Contexts/GameDbContext.cs new file mode 100644 index 0000000..fb4af60 --- /dev/null +++ b/ShadowedRealmsMobile/src/server/ShadowedRealms.Data/Contexts/GameDbContext.cs @@ -0,0 +1,323 @@ +/* + * File: ShadowedRealms.Data/Contexts/GameDbContext.cs + * Created: 2025-10-19 + * Last Modified: 2025-10-19 + * Description: Main Entity Framework database context for Shadowed Realms. Handles all game entities with kingdom-based data partitioning and server-authoritative design. + * Last Edit Notes: Initial creation with basic Player, Alliance, Kingdom entities and kingdom-scoped query filters + */ + +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ShadowedRealms.Core.Models.Alliance; +using ShadowedRealms.Core.Models.Kingdom; +using ShadowedRealms.Core.Models.Player; +using System.Linq.Expressions; +using System.Numerics; + +namespace ShadowedRealms.Data.Contexts +{ + public class GameDbContext : IdentityDbContext, int> + { + private readonly ILogger _logger; + private int? _currentKingdomId; + + public GameDbContext(DbContextOptions options, ILogger logger) + : base(options) + { + _logger = logger; + } + + // Core Game Entities + public DbSet Players { get; set; } + public DbSet Alliances { get; set; } + public DbSet Kingdoms { get; set; } + public DbSet CombatLogs { get; set; } + public DbSet PurchaseLogs { get; set; } + + // Kingdom Context Management + public void SetKingdomContext(int kingdomId) + { + _currentKingdomId = kingdomId; + _logger.LogInformation("Kingdom context set to {KingdomId}", kingdomId); + } + + public int? GetCurrentKingdomId() + { + return _currentKingdomId; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + try + { + // Configure Identity tables with custom names + ConfigureIdentityTables(modelBuilder); + + // Configure core game entities + ConfigurePlayerEntity(modelBuilder); + ConfigureAllianceEntity(modelBuilder); + ConfigureKingdomEntity(modelBuilder); + ConfigureCombatLogEntity(modelBuilder); + ConfigurePurchaseLogEntity(modelBuilder); + + // Apply kingdom-scoped global query filters + ApplyKingdomScopedFilters(modelBuilder); + + // Configure indexes for performance + ConfigureIndexes(modelBuilder); + + _logger.LogInformation("Database model configuration completed successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to configure database model"); + throw; + } + } + + private void ConfigureIdentityTables(ModelBuilder modelBuilder) + { + // Rename Identity tables to match our naming convention + modelBuilder.Entity().ToTable("Players_Auth"); + modelBuilder.Entity>().ToTable("Roles"); + modelBuilder.Entity>().ToTable("Player_Roles"); + modelBuilder.Entity>().ToTable("Player_Claims"); + modelBuilder.Entity>().ToTable("Player_Logins"); + modelBuilder.Entity>().ToTable("Role_Claims"); + modelBuilder.Entity>().ToTable("Player_Tokens"); + } + + private void ConfigurePlayerEntity(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(p => p.Id); + entity.Property(p => p.Name).IsRequired().HasMaxLength(50); + entity.Property(p => p.CastleLevel).IsRequired().HasDefaultValue(1); + entity.Property(p => p.Power).IsRequired().HasDefaultValue(0); + entity.Property(p => p.KingdomId).IsRequired(); + entity.Property(p => p.CreatedAt).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(p => p.LastActiveAt).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(p => p.IsActive).IsRequired().HasDefaultValue(true); + + // Relationships + entity.HasOne(p => p.Kingdom) + .WithMany(k => k.Players) + .HasForeignKey(p => p.KingdomId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(p => p.Alliance) + .WithMany(a => a.Members) + .HasForeignKey(p => p.AllianceId) + .OnDelete(DeleteBehavior.SetNull); + + // Indexes + entity.HasIndex(p => p.KingdomId); + entity.HasIndex(p => p.AllianceId); + entity.HasIndex(p => p.Name); + entity.HasIndex(p => p.Power); + }); + } + + private void ConfigureAllianceEntity(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(a => a.Id); + entity.Property(a => a.Name).IsRequired().HasMaxLength(50); + entity.Property(a => a.Tag).IsRequired().HasMaxLength(5); + entity.Property(a => a.Level).IsRequired().HasDefaultValue(1); + entity.Property(a => a.Power).IsRequired().HasDefaultValue(0); + entity.Property(a => a.KingdomId).IsRequired(); + entity.Property(a => a.CreatedAt).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(a => a.IsActive).IsRequired().HasDefaultValue(true); + entity.Property(a => a.MaxMembers).IsRequired().HasDefaultValue(50); + + // Relationships + entity.HasOne(a => a.Kingdom) + .WithMany(k => k.Alliances) + .HasForeignKey(a => a.KingdomId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(a => a.Leader) + .WithOne() + .HasForeignKey(a => a.LeaderId) + .OnDelete(DeleteBehavior.Restrict); + + // Indexes + entity.HasIndex(a => a.KingdomId); + entity.HasIndex(a => a.Name); + entity.HasIndex(a => a.Tag); + entity.HasIndex(a => a.Power); + }); + } + + private void ConfigureKingdomEntity(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(k => k.Id); + entity.Property(k => k.Name).IsRequired().HasMaxLength(50); + entity.Property(k => k.Number).IsRequired(); + entity.Property(k => k.CreatedAt).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(k => k.IsActive).IsRequired().HasDefaultValue(true); + entity.Property(k => k.MaxPopulation).IsRequired().HasDefaultValue(1500); + entity.Property(k => k.CurrentPopulation).IsRequired().HasDefaultValue(0); + + // Indexes + entity.HasIndex(k => k.Number).IsUnique(); + entity.HasIndex(k => k.Name); + }); + } + + private void ConfigureCombatLogEntity(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(c => c.Id); + entity.Property(c => c.AttackerPlayerId).IsRequired(); + entity.Property(c => c.DefenderPlayerId).IsRequired(); + entity.Property(c => c.KingdomId).IsRequired(); + entity.Property(c => c.CombatType).IsRequired(); + entity.Property(c => c.Result).IsRequired(); + entity.Property(c => c.Timestamp).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + + // Relationships + entity.HasOne() + .WithMany() + .HasForeignKey(c => c.AttackerPlayerId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne() + .WithMany() + .HasForeignKey(c => c.DefenderPlayerId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne() + .WithMany() + .HasForeignKey(c => c.KingdomId) + .OnDelete(DeleteBehavior.Restrict); + + // Indexes + entity.HasIndex(c => c.KingdomId); + entity.HasIndex(c => c.AttackerPlayerId); + entity.HasIndex(c => c.DefenderPlayerId); + entity.HasIndex(c => c.Timestamp); + }); + } + + private void ConfigurePurchaseLogEntity(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(p => p.Id); + entity.Property(p => p.PlayerId).IsRequired(); + entity.Property(p => p.KingdomId).IsRequired(); + entity.Property(p => p.ProductId).IsRequired().HasMaxLength(100); + entity.Property(p => p.Amount).IsRequired().HasColumnType("decimal(18,2)"); + entity.Property(p => p.Currency).IsRequired().HasMaxLength(10); + entity.Property(p => p.Timestamp).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP"); + + // Relationships + entity.HasOne() + .WithMany() + .HasForeignKey(p => p.PlayerId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne() + .WithMany() + .HasForeignKey(p => p.KingdomId) + .OnDelete(DeleteBehavior.Restrict); + + // Indexes + entity.HasIndex(p => p.KingdomId); + entity.HasIndex(p => p.PlayerId); + entity.HasIndex(p => p.Timestamp); + }); + } + + private void ApplyKingdomScopedFilters(ModelBuilder modelBuilder) + { + // Apply global query filters to ensure kingdom-scoped data access + // Players filter + modelBuilder.Entity() + .HasQueryFilter(p => _currentKingdomId == null || p.KingdomId == _currentKingdomId); + + // Alliances filter + modelBuilder.Entity() + .HasQueryFilter(a => _currentKingdomId == null || a.KingdomId == _currentKingdomId); + + // Combat logs filter + modelBuilder.Entity() + .HasQueryFilter(c => _currentKingdomId == null || c.KingdomId == _currentKingdomId); + + // Purchase logs filter + modelBuilder.Entity() + .HasQueryFilter(p => _currentKingdomId == null || p.KingdomId == _currentKingdomId); + } + + private void ConfigureIndexes(ModelBuilder modelBuilder) + { + // Composite indexes for common query patterns + modelBuilder.Entity() + .HasIndex(p => new { p.KingdomId, p.Power }) + .HasDatabaseName("IX_Players_Kingdom_Power"); + + modelBuilder.Entity() + .HasIndex(p => new { p.KingdomId, p.AllianceId }) + .HasDatabaseName("IX_Players_Kingdom_Alliance"); + + modelBuilder.Entity() + .HasIndex(a => new { a.KingdomId, a.Power }) + .HasDatabaseName("IX_Alliances_Kingdom_Power"); + + modelBuilder.Entity() + .HasIndex(c => new { c.KingdomId, c.Timestamp }) + .HasDatabaseName("IX_CombatLogs_Kingdom_Time"); + + modelBuilder.Entity() + .HasIndex(p => new { p.KingdomId, p.Timestamp }) + .HasDatabaseName("IX_PurchaseLogs_Kingdom_Time"); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + try + { + var result = await base.SaveChangesAsync(cancellationToken); + + if (result > 0) + { + _logger.LogInformation("Successfully saved {ChangeCount} changes to database", result); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save changes to database"); + throw; + } + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + _logger.LogWarning("DbContext is not configured - this should only happen during design-time operations"); + } + } + } + + // Custom ApplicationUser for Identity integration + public class ApplicationUser : IdentityUser + { + public int? PlayerId { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime LastLoginAt { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file