using LocationsApi.Models; using MongoDB.Driver; namespace LocationsApi.Services; public class UnitTrainingService { private const string ScoutUnitType = "Scout"; private const string ArcherUnitType = "Archer"; private readonly IMongoCollection _training; private readonly IMongoCollection _locations; private readonly ResourceService _resources; private readonly GameSettingsService _settings; private readonly int _scoutGoldCost; private readonly int _archerGoldCost; private readonly int _archerWoodCost; private readonly int _defaultScoutTrainingSeconds; private readonly int _defaultArcherTrainingSeconds; public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings) { var connectionString = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler"; var client = new MongoClient(connectionString); var database = client.GetDatabase(databaseName); _training = database.GetCollection("UnitTraining"); _locations = database.GetCollection("Locations"); _resources = resources; _settings = settings; _scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50; _archerGoldCost = int.TryParse(cfg["Units:ArcherGoldCost"], out var archerGoldCost) ? Math.Max(0, archerGoldCost) : 100; _archerWoodCost = int.TryParse(cfg["Units:ArcherWoodCost"], out var archerWoodCost) ? Math.Max(0, archerWoodCost) : 100; _defaultScoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds) ? Math.Max(1, scoutTrainingSeconds) : 60; _defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds) ? Math.Max(1, archerTrainingSeconds) : 60; _training.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status))); } public async Task GetMineAsync(string username) { await CompleteFinishedTrainingAsync(username); await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed"); var all = await _training.Find(x => x.Username == username).ToListAsync(); var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed"); var completedArchers = all.Count(x => x.UnitType == ArcherUnitType && x.Status == "Completed"); var activeTraining = all .Where(x => x.Status == "Training") .OrderBy(x => x.CompletesAtUtc) .Select(UnitTrainingResponse.FromTraining) .ToList(); return new PlayerUnitsResponse { CompletedScouts = completedScouts, CompletedArchers = completedArchers, Training = activeTraining }; } public async Task TrainAsync(string username, string unitType) { var normalizedUnitType = NormalizeUnitType(unitType); if (normalizedUnitType is null) { throw new ArgumentException("Unsupported unit type.", nameof(unitType)); } if (normalizedUnitType == ArcherUnitType && !await HasBuiltBarracksAsync(username)) { throw new InvalidOperationException("A completed Barracks is required to train Archers."); } var goldCost = normalizedUnitType == ArcherUnitType ? _archerGoldCost : _scoutGoldCost; var woodCost = normalizedUnitType == ArcherUnitType ? _archerWoodCost : 0; var spent = normalizedUnitType == ArcherUnitType ? await _resources.TrySpendResourcesAsync(username, goldCost, woodCost, 0) : await _resources.TrySpendGoldAsync(username, goldCost); if (!spent) { return null; } var now = DateTime.UtcNow; var trainingSeconds = normalizedUnitType == ArcherUnitType ? await _settings.GetIntAsync(GameSettingsService.ArcherTrainingSeconds, _defaultArcherTrainingSeconds) : await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds); var training = new UnitTraining { Username = username, UnitType = normalizedUnitType, GoldCost = goldCost, WoodCost = woodCost, Status = "Training", StartedAtUtc = now, CompletesAtUtc = now.AddSeconds(trainingSeconds), CreatedAtUtc = now, UpdatedAtUtc = now }; await _training.InsertOneAsync(training); return training; } private async Task HasBuiltBarracksAsync(string username) { await CompleteFinishedBuildingsAsync(username); return await _locations.Find(x => x.OwnerUsername == username && x.BuildingType == "Barracks" && (x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "")) .AnyAsync(); } private async Task CompleteFinishedBuildingsAsync(string username) { var now = DateTime.UtcNow; var update = Builders.Update .Set(x => x.BuildingStatus, "Built") .Set(x => x.BuildingBuiltAtUtc, now) .Set(x => x.UpdatedAtUtc, now); await _locations.UpdateManyAsync( x => x.OwnerUsername == username && x.BuildingType == "Barracks" && x.BuildingStatus == "Building" && x.BuildingCompletesAtUtc <= now, update); } public async Task TryConsumeCompletedScoutAsync(string username) { await CompleteFinishedTrainingAsync(username); var consumed = await _training.FindOneAndDeleteAsync( x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed", new FindOneAndDeleteOptions { Sort = Builders.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc) }); return consumed is not null; } public async Task HasCompletedScoutAsync(string username) { await CompleteFinishedTrainingAsync(username); return await _training.Find(x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed").AnyAsync(); } private async Task CompleteFinishedTrainingAsync(string username) { var now = DateTime.UtcNow; var update = Builders.Update .Set(x => x.Status, "Completed") .Set(x => x.CompletedAtUtc, now) .Set(x => x.UpdatedAtUtc, now); await _training.UpdateManyAsync( x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now, update); } private static string? NormalizeUnitType(string unitType) { var trimmed = unitType.Trim(); return trimmed.ToLowerInvariant() switch { "scout" => ScoutUnitType, "archer" => ArcherUnitType, _ => null }; } }