Core Foundation: Complete entity models and database context

- Add GameDbContext with kingdom-scoped query filters and Identity integration
- Add Kingdom model with population management and KvK tracking
- Add Player model with complete progression system (Castle 1-44, T1-T13 troops)
- Add Alliance model with research trees, territory system, and coalition mechanics
- Add CombatLog model with field interception and comprehensive battle tracking
- Add PurchaseLog model with enhanced chargeback protection and customer segmentation
- Add VipRewards model with KoA-inspired secret tier system for whale retention

Features implemented:
* Server-authoritative design with comprehensive validation
* Kingdom-based data partitioning for horizontal scaling
* Anti-pay-to-win mechanics with skill-based alternatives
* Monthly/lifetime customer segmentation for actionable insights
* Robust fraud detection and chargeback prevention
* Secret spending tiers and milestone rewards system
* Production-ready models with complete business logic

All models include proper file headers, error handling, and are ready for Entity Framework integration.
This commit is contained in:
matt 2025-10-19 12:04:37 -05:00
parent 3c0d375137
commit a73d15eaa2
7 changed files with 3371 additions and 0 deletions

View File

@ -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<Player.Player> Members { get; set; } = new List<Player.Player>();
public virtual ICollection<AllianceRole> Roles { get; set; } = new List<AllianceRole>();
public virtual ICollection<AllianceInvitation> PendingInvitations { get; set; } = new List<AllianceInvitation>();
// 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<string> ValidateAllianceState()
{
var errors = new List<string>();
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
}
}

View File

@ -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>(DetailedBattleReport);
}
catch
{
return null;
}
}
public List<string> ValidateCombatLog()
{
var errors = new List<string>();
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<BattleRound> Rounds { get; set; } = new List<BattleRound>();
public Dictionary<string, double> AttackerBonuses { get; set; } = new Dictionary<string, double>();
public Dictionary<string, double> DefenderBonuses { get; set; } = new Dictionary<string, double>();
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
}
}

View File

@ -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<Player.Player> Players { get; set; } = new List<Player.Player>();
public virtual ICollection<Alliance.Alliance> Alliances { get; set; } = new List<Alliance.Alliance>();
// 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<string> ValidateKingdomSettings()
{
var errors = new List<string>();
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
}
}

View File

@ -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<string> ValidatePlayerState()
{
var errors = new List<string>();
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
}
}

View File

@ -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<VipMilestone> MilestonesReached { get; set; } = new List<VipMilestone>();
public virtual ICollection<VipReward> RewardsClaimed { get; set; } = new List<VipReward>();
// 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<string>();
// 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<VipRewardTier> GetAvailableRewards()
{
var rewards = new List<VipRewardTier>();
// 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
}
}

View File

@ -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>(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<string> ValidatePurchaseLog()
{
var errors = new List<string>();
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<string, object> PlatformData { get; set; } = new Dictionary<string, object>();
public List<string> ItemsReceived { get; set; } = new List<string>();
public Dictionary<string, double> BonusMultipliers { get; set; } = new Dictionary<string, double>();
public string? SpecialOfferInfo { get; set; }
public DateTime? OfferExpiryDate { get; set; }
public List<string> RequirementsMet { get; set; } = new List<string>();
public Dictionary<string, string> RiskIndicators { get; set; } = new Dictionary<string, string>();
public Dictionary<string, int> VipMilestoneData { get; set; } = new Dictionary<string, int>();
}
// 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
}
}

View File

@ -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<ApplicationUser, IdentityRole<int>, int>
{
private readonly ILogger<GameDbContext> _logger;
private int? _currentKingdomId;
public GameDbContext(DbContextOptions<GameDbContext> options, ILogger<GameDbContext> logger)
: base(options)
{
_logger = logger;
}
// Core Game Entities
public DbSet<Player> Players { get; set; }
public DbSet<Alliance> Alliances { get; set; }
public DbSet<Kingdom> Kingdoms { get; set; }
public DbSet<CombatLog> CombatLogs { get; set; }
public DbSet<PurchaseLog> 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<ApplicationUser>().ToTable("Players_Auth");
modelBuilder.Entity<IdentityRole<int>>().ToTable("Roles");
modelBuilder.Entity<IdentityUserRole<int>>().ToTable("Player_Roles");
modelBuilder.Entity<IdentityUserClaim<int>>().ToTable("Player_Claims");
modelBuilder.Entity<IdentityUserLogin<int>>().ToTable("Player_Logins");
modelBuilder.Entity<IdentityRoleClaim<int>>().ToTable("Role_Claims");
modelBuilder.Entity<IdentityUserToken<int>>().ToTable("Player_Tokens");
}
private void ConfigurePlayerEntity(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Player>(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<Alliance>(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<Alliance>(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<Kingdom>(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<CombatLog>(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<Player>()
.WithMany()
.HasForeignKey(c => c.AttackerPlayerId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Player>()
.WithMany()
.HasForeignKey(c => c.DefenderPlayerId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Kingdom>()
.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<PurchaseLog>(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<Player>()
.WithMany()
.HasForeignKey(p => p.PlayerId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Kingdom>()
.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<Player>()
.HasQueryFilter(p => _currentKingdomId == null || p.KingdomId == _currentKingdomId);
// Alliances filter
modelBuilder.Entity<Alliance>()
.HasQueryFilter(a => _currentKingdomId == null || a.KingdomId == _currentKingdomId);
// Combat logs filter
modelBuilder.Entity<CombatLog>()
.HasQueryFilter(c => _currentKingdomId == null || c.KingdomId == _currentKingdomId);
// Purchase logs filter
modelBuilder.Entity<PurchaseLog>()
.HasQueryFilter(p => _currentKingdomId == null || p.KingdomId == _currentKingdomId);
}
private void ConfigureIndexes(ModelBuilder modelBuilder)
{
// Composite indexes for common query patterns
modelBuilder.Entity<Player>()
.HasIndex(p => new { p.KingdomId, p.Power })
.HasDatabaseName("IX_Players_Kingdom_Power");
modelBuilder.Entity<Player>()
.HasIndex(p => new { p.KingdomId, p.AllianceId })
.HasDatabaseName("IX_Players_Kingdom_Alliance");
modelBuilder.Entity<Alliance>()
.HasIndex(a => new { a.KingdomId, a.Power })
.HasDatabaseName("IX_Alliances_Kingdom_Power");
modelBuilder.Entity<CombatLog>()
.HasIndex(c => new { c.KingdomId, c.Timestamp })
.HasDatabaseName("IX_CombatLogs_Kingdom_Time");
modelBuilder.Entity<PurchaseLog>()
.HasIndex(p => new { p.KingdomId, p.Timestamp })
.HasDatabaseName("IX_PurchaseLogs_Kingdom_Time");
}
public override async Task<int> 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<int>
{
public int? PlayerId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastLoginAt { get; set; } = DateTime.UtcNow;
}
}