181 lines
7.3 KiB
C#
181 lines
7.3 KiB
C#
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<UnitTraining> _training;
|
|
private readonly IMongoCollection<Location> _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>("UnitTraining");
|
|
_locations = database.GetCollection<Location>("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<UnitTraining>(
|
|
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
|
|
}
|
|
|
|
public async Task<PlayerUnitsResponse> 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<UnitTraining?> 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<bool> 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<Location>.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<bool> TryConsumeCompletedScoutAsync(string username)
|
|
{
|
|
await CompleteFinishedTrainingAsync(username);
|
|
|
|
var consumed = await _training.FindOneAndDeleteAsync(
|
|
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
|
new FindOneAndDeleteOptions<UnitTraining>
|
|
{
|
|
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc)
|
|
});
|
|
|
|
return consumed is not null;
|
|
}
|
|
|
|
public async Task<bool> 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<UnitTraining>.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
|
|
};
|
|
}
|
|
}
|