tracking units
This commit is contained in:
@@ -42,7 +42,7 @@ public class UnitsController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
var training = await _units.TrainAsync(username, request.UnitType);
|
||||
var training = await _units.TrainAsync(username, request);
|
||||
if (training is null)
|
||||
{
|
||||
return BadRequest(new { error = "Not enough resources." });
|
||||
|
||||
@@ -3,6 +3,8 @@ namespace LocationsApi.Models;
|
||||
public class TrainUnitRequest
|
||||
{
|
||||
public string UnitType { get; set; } = "Scout";
|
||||
public int? Q { get; set; }
|
||||
public int? R { get; set; }
|
||||
}
|
||||
|
||||
public class UnitTrainingResponse
|
||||
@@ -13,6 +15,8 @@ public class UnitTrainingResponse
|
||||
public int GoldCost { get; set; }
|
||||
public int WoodCost { get; set; }
|
||||
public int OreCost { get; set; }
|
||||
public int? OriginQ { get; set; }
|
||||
public int? OriginR { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public DateTime StartedAtUtc { get; set; }
|
||||
public DateTime CompletesAtUtc { get; set; }
|
||||
@@ -26,6 +30,8 @@ public class UnitTrainingResponse
|
||||
GoldCost = training.GoldCost,
|
||||
WoodCost = training.WoodCost,
|
||||
OreCost = training.OreCost,
|
||||
OriginQ = training.OriginQ,
|
||||
OriginR = training.OriginR,
|
||||
Status = training.Status,
|
||||
StartedAtUtc = training.StartedAtUtc,
|
||||
CompletesAtUtc = training.CompletesAtUtc,
|
||||
@@ -33,9 +39,34 @@ public class UnitTrainingResponse
|
||||
};
|
||||
}
|
||||
|
||||
public class UnitInstanceResponse
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Username { get; set; } = "";
|
||||
public string UnitType { get; set; } = "";
|
||||
public int Q { get; set; }
|
||||
public int R { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
public static UnitInstanceResponse FromUnit(UnitInstance unit) => new()
|
||||
{
|
||||
Id = unit.Id,
|
||||
Username = unit.Username,
|
||||
UnitType = unit.UnitType,
|
||||
Q = unit.Q,
|
||||
R = unit.R,
|
||||
Status = unit.Status,
|
||||
CreatedAtUtc = unit.CreatedAtUtc,
|
||||
UpdatedAtUtc = unit.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
public class PlayerUnitsResponse
|
||||
{
|
||||
public int CompletedScouts { get; set; }
|
||||
public int CompletedArchers { get; set; }
|
||||
public List<UnitTrainingResponse> Training { get; set; } = [];
|
||||
public List<UnitInstanceResponse> Units { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace LocationsApi.Models;
|
||||
|
||||
public class UnitInstance
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
public string Username { get; set; } = "";
|
||||
public string UnitType { get; set; } = "Archer";
|
||||
public int Q { get; set; }
|
||||
public int R { get; set; }
|
||||
public string Status { get; set; } = "Idle";
|
||||
public string? SourceTrainingId { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ public class UnitTraining
|
||||
public int GoldCost { get; set; } = 50;
|
||||
public int WoodCost { get; set; }
|
||||
public int OreCost { get; set; }
|
||||
public int? OriginQ { get; set; }
|
||||
public int? OriginR { get; set; }
|
||||
public string Status { get; set; } = "Training";
|
||||
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CompletesAtUtc { get; set; } = DateTime.UtcNow.AddMinutes(1);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user