tracking units

This commit is contained in:
hz
2026-05-19 05:20:28 -05:00
parent e78eb24543
commit b0b270f4d8
8 changed files with 258 additions and 22 deletions
@@ -165,6 +165,11 @@ public class LocationService
throw new InvalidOperationException("This location already has a building or construction in progress.");
}
if (buildingType == "Barracks" && string.Equals(location.Kind, "Home", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Barracks cannot be built on the town hall.");
}
var requiredTerrain = RequiredTerrainForBuilding(buildingType);
if (requiredTerrain is not null && !string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
{
@@ -9,6 +9,7 @@ public class UnitTrainingService
private const string ArcherUnitType = "Archer";
private readonly IMongoCollection<UnitTraining> _training;
private readonly IMongoCollection<UnitInstance> _units;
private readonly IMongoCollection<Location> _locations;
private readonly ResourceService _resources;
private readonly GameSettingsService _settings;
@@ -25,6 +26,7 @@ public class UnitTrainingService
var client = new MongoClient(connectionString);
var database = client.GetDatabase(databaseName);
_training = database.GetCollection<UnitTraining>("UnitTraining");
_units = database.GetCollection<UnitInstance>("Units");
_locations = database.GetCollection<Location>("Locations");
_resources = resources;
_settings = settings;
@@ -40,6 +42,15 @@ public class UnitTrainingService
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
_units.Indexes.CreateOne(new CreateIndexModel<UnitInstance>(
Builders<UnitInstance>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.UnitType)));
_units.Indexes.CreateOne(new CreateIndexModel<UnitInstance>(
Builders<UnitInstance>.IndexKeys.Ascending(x => x.SourceTrainingId),
new CreateIndexOptions<UnitInstance>
{
Unique = true,
Sparse = true
}));
}
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
@@ -47,10 +58,11 @@ public class UnitTrainingService
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
var allTraining = await _training.Find(x => x.Username == username).ToListAsync();
var allUnits = await _units.Find(x => x.Username == username).ToListAsync();
var completedScouts = allTraining.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
var completedArchers = allUnits.Count(x => x.UnitType == ArcherUnitType);
var activeTraining = allTraining
.Where(x => x.Status == "Training")
.OrderBy(x => x.CompletesAtUtc)
.Select(UnitTrainingResponse.FromTraining)
@@ -60,21 +72,37 @@ public class UnitTrainingService
{
CompletedScouts = completedScouts,
CompletedArchers = completedArchers,
Training = activeTraining
Training = activeTraining,
Units = allUnits
.OrderBy(x => x.Q)
.ThenBy(x => x.R)
.ThenBy(x => x.CreatedAtUtc)
.Select(UnitInstanceResponse.FromUnit)
.ToList()
};
}
public async Task<UnitTraining?> TrainAsync(string username, string unitType)
public async Task<UnitTraining?> TrainAsync(string username, TrainUnitRequest request)
{
var normalizedUnitType = NormalizeUnitType(unitType);
var normalizedUnitType = NormalizeUnitType(request.UnitType);
if (normalizedUnitType is null)
{
throw new ArgumentException("Unsupported unit type.", nameof(unitType));
throw new ArgumentException("Unsupported unit type.", nameof(request.UnitType));
}
if (normalizedUnitType == ArcherUnitType && !await HasBuiltBarracksAsync(username))
Location? origin = null;
if (normalizedUnitType == ArcherUnitType)
{
throw new InvalidOperationException("A completed Barracks is required to train Archers.");
if (request.Q is null || request.R is null)
{
throw new InvalidOperationException("Select a completed Barracks to train Archers.");
}
origin = await GetBuiltBarracksAsync(username, request.Q.Value, request.R.Value);
if (origin is null)
{
throw new InvalidOperationException("A completed Barracks is required to train Archers.");
}
}
var goldCost = normalizedUnitType == ArcherUnitType ? _archerGoldCost : _scoutGoldCost;
@@ -97,6 +125,8 @@ public class UnitTrainingService
UnitType = normalizedUnitType,
GoldCost = goldCost,
WoodCost = woodCost,
OriginQ = origin?.Q,
OriginR = origin?.R,
Status = "Training",
StartedAtUtc = now,
CompletesAtUtc = now.AddSeconds(trainingSeconds),
@@ -108,14 +138,16 @@ public class UnitTrainingService
return training;
}
private async Task<bool> HasBuiltBarracksAsync(string username)
private async Task<Location?> GetBuiltBarracksAsync(string username, int q, int r)
{
await CompleteFinishedBuildingsAsync(username);
return await _locations.Find(x =>
x.OwnerUsername == username &&
x.Q == q &&
x.R == r &&
x.BuildingType == "Barracks" &&
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == ""))
.AnyAsync();
.FirstOrDefaultAsync();
}
private async Task CompleteFinishedBuildingsAsync(string username)
@@ -157,14 +189,52 @@ public class UnitTrainingService
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);
var readyTraining = await _training.Find(x =>
x.Username == username &&
x.Status == "Training" &&
x.CompletesAtUtc <= now)
.ToListAsync();
await _training.UpdateManyAsync(
x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now,
update);
foreach (var training in readyTraining)
{
var update = Builders<UnitTraining>.Update
.Set(x => x.Status, "Completed")
.Set(x => x.CompletedAtUtc, now)
.Set(x => x.UpdatedAtUtc, now);
var completed = await _training.FindOneAndUpdateAsync(
x => x.Id == training.Id && x.Status == "Training",
update,
new FindOneAndUpdateOptions<UnitTraining>
{
ReturnDocument = ReturnDocument.After
});
if (completed is null || completed.UnitType != ArcherUnitType)
{
continue;
}
var unit = new UnitInstance
{
Username = completed.Username,
UnitType = ArcherUnitType,
Q = completed.OriginQ ?? 0,
R = completed.OriginR ?? 0,
Status = "Idle",
SourceTrainingId = completed.Id,
CreatedAtUtc = now,
UpdatedAtUtc = now
};
try
{
await _units.InsertOneAsync(unit);
}
catch (MongoWriteException)
{
}
}
}
private static string? NormalizeUnitType(string unitType)