Adding upkeep
This commit is contained in:
@@ -5,8 +5,14 @@ namespace LocationsApi.Services;
|
||||
|
||||
public class ResourceService
|
||||
{
|
||||
private const string ScoutUnitType = "Scout";
|
||||
private const string ArcherUnitType = "Archer";
|
||||
private const string PeasantUnitType = "Peasant";
|
||||
|
||||
private readonly IMongoCollection<PlayerResources> _resources;
|
||||
private readonly IMongoCollection<Location> _locations;
|
||||
private readonly IMongoCollection<UnitTraining> _training;
|
||||
private readonly IMongoCollection<UnitInstance> _units;
|
||||
private readonly GameSettingsService _settings;
|
||||
private readonly int _startingGold;
|
||||
private readonly int _headquartersGoldPerMinute;
|
||||
@@ -15,6 +21,7 @@ public class ResourceService
|
||||
private readonly int _townHallOreCapacity;
|
||||
private readonly int _townHallFoodCapacity;
|
||||
private readonly int _productionBuildingResourceCapacity;
|
||||
private readonly int _unitFoodPerMinute;
|
||||
|
||||
public ResourceService(IConfiguration cfg, GameSettingsService settings)
|
||||
{
|
||||
@@ -24,6 +31,8 @@ public class ResourceService
|
||||
var database = client.GetDatabase(databaseName);
|
||||
_resources = database.GetCollection<PlayerResources>("PlayerResources");
|
||||
_locations = database.GetCollection<Location>("Locations");
|
||||
_training = database.GetCollection<UnitTraining>("UnitTraining");
|
||||
_units = database.GetCollection<UnitInstance>("Units");
|
||||
_settings = settings;
|
||||
_startingGold = int.TryParse(cfg["Resources:StartingGold"], out var startingGold) ? Math.Max(0, startingGold) : 100;
|
||||
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
||||
@@ -44,6 +53,9 @@ public class ResourceService
|
||||
_productionBuildingResourceCapacity = int.TryParse(cfg["Buildings:ProductionBuildingResourceCapacity"], out var productionBuildingResourceCapacity)
|
||||
? Math.Max(0, productionBuildingResourceCapacity)
|
||||
: 10;
|
||||
_unitFoodPerMinute = int.TryParse(cfg["Units:UnitFoodPerMinute"], out var unitFoodPerMinute)
|
||||
? Math.Max(0, unitFoodPerMinute)
|
||||
: 1;
|
||||
|
||||
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
||||
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
||||
@@ -92,7 +104,8 @@ public class ResourceService
|
||||
public async Task<PlayerResources> GetAsync(string username)
|
||||
{
|
||||
var resources = await EnsureAsync(username);
|
||||
return await ApplyProductionBuildingDeliveryAsync(resources);
|
||||
resources = await ApplyProductionBuildingDeliveryAsync(resources);
|
||||
return await ApplyFoodUpkeepAsync(resources);
|
||||
}
|
||||
|
||||
public async Task<bool> TrySpendGoldAsync(string username, int amount)
|
||||
@@ -300,6 +313,7 @@ public class ResourceService
|
||||
private async Task<PlayerResources> ApplyProductionBuildingDeliveryAsync(PlayerResources resources)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
await CompletePendingWorkerAssignmentsAsync(resources.Username, now);
|
||||
var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity);
|
||||
var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity);
|
||||
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
||||
@@ -377,4 +391,239 @@ public class ResourceService
|
||||
ReturnDocument = ReturnDocument.After
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<PlayerResources> ApplyFoodUpkeepAsync(PlayerResources resources)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var unitFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.UnitFoodPerMinute, _unitFoodPerMinute);
|
||||
var foodProductionPerMinute = await CalculateFoodProductionPerMinuteAsync(resources.Username);
|
||||
var units = await GetFoodConsumingUnitsAsync(resources.Username);
|
||||
var upkeepPerMinute = units.Count * unitFoodPerMinute;
|
||||
var netFoodPerMinute = foodProductionPerMinute - upkeepPerMinute;
|
||||
var elapsedMinutes = upkeepPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000
|
||||
? 0
|
||||
: (int)Math.Floor((now - resources.LastFoodGeneratedAtUtc).TotalMinutes);
|
||||
var food = resources.Food;
|
||||
var removedUnits = new List<FoodConsumingUnit>();
|
||||
|
||||
for (var minute = 0; minute < elapsedMinutes && units.Count > 0; minute++)
|
||||
{
|
||||
var neededFood = units.Count * unitFoodPerMinute;
|
||||
if (food >= neededFood)
|
||||
{
|
||||
food -= neededFood;
|
||||
continue;
|
||||
}
|
||||
|
||||
var shortage = neededFood - food;
|
||||
food = 0;
|
||||
var unitsToRemove = Math.Min(shortage, units.Count);
|
||||
for (var i = 0; i < unitsToRemove; i++)
|
||||
{
|
||||
var index = Random.Shared.Next(units.Count);
|
||||
removedUnits.Add(units[index]);
|
||||
units.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
netFoodPerMinute = foodProductionPerMinute - (units.Count * unitFoodPerMinute);
|
||||
if (elapsedMinutes <= 0 && removedUnits.Count == 0 && resources.Food == food && resources.FoodPerMinute == netFoodPerMinute)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
|
||||
await RemoveStarvedUnitsAsync(resources.Username, removedUnits, now);
|
||||
|
||||
var update = Builders<PlayerResources>.Update
|
||||
.Set(x => x.FoodPerMinute, netFoodPerMinute)
|
||||
.Set(x => x.Food, food)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
if (elapsedMinutes > 0)
|
||||
{
|
||||
update = update.Set(x => x.LastFoodGeneratedAtUtc, resources.LastFoodGeneratedAtUtc.AddMinutes(elapsedMinutes));
|
||||
}
|
||||
|
||||
return await _resources.FindOneAndUpdateAsync(
|
||||
x => x.Username == resources.Username,
|
||||
update,
|
||||
new FindOneAndUpdateOptions<PlayerResources>
|
||||
{
|
||||
ReturnDocument = ReturnDocument.After
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<List<FoodConsumingUnit>> GetFoodConsumingUnitsAsync(string username)
|
||||
{
|
||||
var completedScouts = await _training.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ScoutUnitType &&
|
||||
x.Status == "Completed")
|
||||
.ToListAsync();
|
||||
var persistedUnits = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
(x.UnitType == ArcherUnitType || x.UnitType == PeasantUnitType))
|
||||
.ToListAsync();
|
||||
|
||||
var units = completedScouts
|
||||
.Select(x => new FoodConsumingUnit(x.Id, x.UnitType, true))
|
||||
.ToList();
|
||||
units.AddRange(persistedUnits.Select(x => new FoodConsumingUnit(x.Id, x.UnitType, false)));
|
||||
|
||||
return units;
|
||||
}
|
||||
|
||||
private async Task<int> CalculateFoodProductionPerMinuteAsync(string username)
|
||||
{
|
||||
var farmFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, 5);
|
||||
var farms = await _locations.CountDocumentsAsync(x =>
|
||||
x.OwnerUsername == username &&
|
||||
!string.IsNullOrWhiteSpace(x.WorkerUnitId) &&
|
||||
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") &&
|
||||
x.BuildingType == "Farm");
|
||||
|
||||
return checked((int)farms) * farmFoodPerMinute;
|
||||
}
|
||||
|
||||
private async Task CompletePendingWorkerAssignmentsAsync(string username, DateTime now)
|
||||
{
|
||||
var movingWorkers = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == "Peasant" &&
|
||||
x.Status == "MovingToWork" &&
|
||||
x.MovementCompletesAtUtc != null &&
|
||||
x.MovementCompletesAtUtc <= now)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var worker in movingWorkers)
|
||||
{
|
||||
if (worker.DestinationQ is null || worker.DestinationR is null)
|
||||
{
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == worker.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Unset(x => x.MovementStartedAtUtc)
|
||||
.Unset(x => x.MovementCompletesAtUtc)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
continue;
|
||||
}
|
||||
|
||||
var destinationQ = worker.DestinationQ.Value;
|
||||
var destinationR = worker.DestinationR.Value;
|
||||
var currentQ = worker.Q;
|
||||
var currentR = worker.R;
|
||||
var nextTick = worker.MovementCompletesAtUtc!.Value;
|
||||
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||
{
|
||||
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
|
||||
nextTick = nextTick.AddMinutes(1);
|
||||
}
|
||||
|
||||
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||
{
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == worker.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, currentQ)
|
||||
.Set(x => x.R, currentR)
|
||||
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
continue;
|
||||
}
|
||||
|
||||
var location = await _locations.FindOneAndUpdateAsync(
|
||||
x => x.OwnerUsername == username &&
|
||||
x.Q == destinationQ &&
|
||||
x.R == destinationR &&
|
||||
!string.IsNullOrWhiteSpace(x.BuildingType) &&
|
||||
x.BuildingStatus != "Building" &&
|
||||
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||
Builders<Location>.Update
|
||||
.Set(x => x.WorkerUnitId, worker.Id)
|
||||
.Set(x => x.WorkerUnitType, "Peasant")
|
||||
.Set(x => x.LastResourceGeneratedAtUtc, now)
|
||||
.Set(x => x.UpdatedAtUtc, now),
|
||||
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
|
||||
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == worker.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, destinationQ)
|
||||
.Set(x => x.R, destinationR)
|
||||
.Set(x => x.Status, location is null ? "Idle" : "Working")
|
||||
.Set(x => x.LastMovementRefillAtUtc, now)
|
||||
.Unset(x => x.DestinationQ)
|
||||
.Unset(x => x.DestinationR)
|
||||
.Unset(x => x.MovementStartedAtUtc)
|
||||
.Unset(x => x.MovementCompletesAtUtc)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
}
|
||||
}
|
||||
|
||||
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
||||
{
|
||||
return NeighborCoordinates(fromQ, fromR)
|
||||
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
||||
.ThenBy(x => x.q)
|
||||
.ThenBy(x => x.r)
|
||||
.First();
|
||||
}
|
||||
|
||||
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
|
||||
{
|
||||
yield return (q + 1, r);
|
||||
yield return (q + 1, r - 1);
|
||||
yield return (q, r - 1);
|
||||
yield return (q - 1, r);
|
||||
yield return (q - 1, r + 1);
|
||||
yield return (q, r + 1);
|
||||
}
|
||||
|
||||
private static int HexDistance(int q1, int r1, int q2, int r2)
|
||||
{
|
||||
var s1 = -q1 - r1;
|
||||
var s2 = -q2 - r2;
|
||||
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
|
||||
}
|
||||
|
||||
private async Task RemoveStarvedUnitsAsync(string username, List<FoodConsumingUnit> units, DateTime now)
|
||||
{
|
||||
if (units.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scoutIds = units
|
||||
.Where(x => x.IsTrainingRecord)
|
||||
.Select(x => x.Id)
|
||||
.ToList();
|
||||
var instanceIds = units
|
||||
.Where(x => !x.IsTrainingRecord)
|
||||
.Select(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (scoutIds.Count > 0)
|
||||
{
|
||||
await _training.DeleteManyAsync(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ScoutUnitType &&
|
||||
scoutIds.Contains(x.Id));
|
||||
}
|
||||
|
||||
if (instanceIds.Count > 0)
|
||||
{
|
||||
await _locations.UpdateManyAsync(
|
||||
x => x.OwnerUsername == username && x.WorkerUnitId != null && instanceIds.Contains(x.WorkerUnitId),
|
||||
Builders<Location>.Update
|
||||
.Unset(x => x.WorkerUnitId)
|
||||
.Unset(x => x.WorkerUnitType)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
await _units.DeleteManyAsync(x =>
|
||||
x.Username == username &&
|
||||
instanceIds.Contains(x.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record FoodConsumingUnit(string Id, string UnitType, bool IsTrainingRecord);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user