Adding upkeep
This commit is contained in:
@@ -56,6 +56,13 @@ public class UnitInstanceResponse
|
||||
public int Q { get; set; }
|
||||
public int R { get; set; }
|
||||
public string Status { get; set; } = "";
|
||||
public int MovementPerTick { get; set; }
|
||||
public int MovementRemaining { get; set; }
|
||||
public DateTime LastMovementRefillAtUtc { get; set; }
|
||||
public int? DestinationQ { get; set; }
|
||||
public int? DestinationR { get; set; }
|
||||
public DateTime? MovementStartedAtUtc { get; set; }
|
||||
public DateTime? MovementCompletesAtUtc { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
@@ -67,6 +74,13 @@ public class UnitInstanceResponse
|
||||
Q = unit.Q,
|
||||
R = unit.R,
|
||||
Status = unit.Status,
|
||||
MovementPerTick = unit.MovementPerTick,
|
||||
MovementRemaining = unit.MovementRemaining,
|
||||
LastMovementRefillAtUtc = unit.LastMovementRefillAtUtc,
|
||||
DestinationQ = unit.DestinationQ,
|
||||
DestinationR = unit.DestinationR,
|
||||
MovementStartedAtUtc = unit.MovementStartedAtUtc,
|
||||
MovementCompletesAtUtc = unit.MovementCompletesAtUtc,
|
||||
CreatedAtUtc = unit.CreatedAtUtc,
|
||||
UpdatedAtUtc = unit.UpdatedAtUtc
|
||||
};
|
||||
|
||||
@@ -14,6 +14,13 @@ public class UnitInstance
|
||||
public int Q { get; set; }
|
||||
public int R { get; set; }
|
||||
public string Status { get; set; } = "Idle";
|
||||
public int MovementPerTick { get; set; } = 1;
|
||||
public int MovementRemaining { get; set; } = 1;
|
||||
public DateTime LastMovementRefillAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public int? DestinationQ { get; set; }
|
||||
public int? DestinationR { get; set; }
|
||||
public DateTime? MovementStartedAtUtc { get; set; }
|
||||
public DateTime? MovementCompletesAtUtc { get; set; }
|
||||
public string? SourceTrainingId { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
@@ -12,6 +12,8 @@ public class GameSettingsService
|
||||
public const string ArcherGoldCost = "ArcherGoldCost";
|
||||
public const string ArcherWoodCost = "ArcherWoodCost";
|
||||
public const string PeasantGoldCost = "PeasantGoldCost";
|
||||
public const string UnitFoodPerMinute = "UnitFoodPerMinute";
|
||||
public const string UnitMovementPerTick = "UnitMovementPerTick";
|
||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
||||
|
||||
@@ -284,6 +284,7 @@ public class LocationService
|
||||
public async Task<Location?> AssignWorkerAsync(string username, AssignWorkerRequest request)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
await CompletePendingWorkerAssignmentsAsync(username);
|
||||
var location = await GetAsync(request.Q, request.R);
|
||||
if (location is null ||
|
||||
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) ||
|
||||
@@ -308,36 +309,19 @@ public class LocationService
|
||||
var claimed = await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id && x.Status == "Idle",
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, request.Q)
|
||||
.Set(x => x.R, request.R)
|
||||
.Set(x => x.Status, "Working")
|
||||
.Set(x => x.Status, "MovingToWork")
|
||||
.Set(x => x.MovementRemaining, 0)
|
||||
.Set(x => x.DestinationQ, request.Q)
|
||||
.Set(x => x.DestinationR, request.R)
|
||||
.Set(x => x.MovementStartedAtUtc, now)
|
||||
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(Math.Max(1, HexDistance(unit.Q, unit.R, request.Q, request.R))))
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
if (claimed.ModifiedCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var updated = await _locations.FindOneAndUpdateAsync(
|
||||
x => x.Id == location.Id && (x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||
Builders<Location>.Update
|
||||
.Set(x => x.WorkerUnitId, unit.Id)
|
||||
.Set(x => x.WorkerUnitType, "Peasant")
|
||||
.Set(x => x.LastResourceGeneratedAtUtc, now)
|
||||
.Set(x => x.UpdatedAtUtc, now),
|
||||
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
|
||||
|
||||
if (updated is null)
|
||||
{
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow));
|
||||
return null;
|
||||
}
|
||||
|
||||
await RecalculateWorkerGenerationAsync(username);
|
||||
return updated;
|
||||
return location;
|
||||
}
|
||||
|
||||
public async Task<Location?> DeassignWorkerAsync(string username, DeassignWorkerRequest request)
|
||||
@@ -433,6 +417,7 @@ public class LocationService
|
||||
public async Task<List<LocationResponse>> GetVisibleAsync(string username, int? visibilityDepth = null)
|
||||
{
|
||||
await CompleteFinishedBuildingsAsync(username);
|
||||
await CompletePendingWorkerAssignmentsAsync(username);
|
||||
await RecalculateWorkerGenerationAsync(username);
|
||||
|
||||
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||
@@ -500,6 +485,7 @@ public class LocationService
|
||||
|
||||
public async Task<LocationResponse?> GetVisibleLocationAsync(string username, int q, int r)
|
||||
{
|
||||
await CompletePendingWorkerAssignmentsAsync(username);
|
||||
if (!await CanSeeAsync(username, q, r))
|
||||
{
|
||||
return null;
|
||||
@@ -522,6 +508,98 @@ public class LocationService
|
||||
return generated.Count == 0 ? null : LocationResponse.FromLocation(generated[0]);
|
||||
}
|
||||
|
||||
private async Task CompletePendingWorkerAssignmentsAsync(string username)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
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 });
|
||||
|
||||
var status = location is null ? "Idle" : "Working";
|
||||
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, status)
|
||||
.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));
|
||||
}
|
||||
|
||||
if (movingWorkers.Count > 0)
|
||||
{
|
||||
await RecalculateWorkerGenerationAsync(username);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public async Task<bool> CanSeeAsync(string username, int q, int r)
|
||||
{
|
||||
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class UnitTrainingService
|
||||
private readonly int _defaultScoutTrainingSeconds;
|
||||
private readonly int _defaultArcherTrainingSeconds;
|
||||
private readonly int _defaultPeasantTrainingSeconds;
|
||||
private readonly int _unitMovementPerTick;
|
||||
private readonly int _visibilityDepth;
|
||||
|
||||
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||
@@ -47,6 +48,9 @@ public class UnitTrainingService
|
||||
_defaultPeasantTrainingSeconds = int.TryParse(cfg["Units:PeasantTrainingSeconds"], out var peasantTrainingSeconds)
|
||||
? Math.Max(1, peasantTrainingSeconds)
|
||||
: 60;
|
||||
_unitMovementPerTick = int.TryParse(cfg["Units:UnitMovementPerTick"], out var unitMovementPerTick)
|
||||
? Math.Max(1, unitMovementPerTick)
|
||||
: 1;
|
||||
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
|
||||
|
||||
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
||||
@@ -65,6 +69,8 @@ public class UnitTrainingService
|
||||
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
await CompletePendingMovementAsync(username);
|
||||
await RefillMovementAsync(username);
|
||||
await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed");
|
||||
|
||||
var allTraining = await _training.Find(x => x.Username == username).ToListAsync();
|
||||
@@ -167,6 +173,9 @@ public class UnitTrainingService
|
||||
|
||||
public async Task<List<UnitInstance>> MoveSquadAsync(string username, MoveUnitSquadRequest request)
|
||||
{
|
||||
await CompletePendingMovementAsync(username);
|
||||
await RefillMovementAsync(username);
|
||||
|
||||
var normalizedUnitType = NormalizeUnitType(request.UnitType);
|
||||
if (normalizedUnitType != ArcherUnitType)
|
||||
{
|
||||
@@ -192,6 +201,7 @@ public class UnitTrainingService
|
||||
var units = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Status == "Idle" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR)
|
||||
.ToListAsync();
|
||||
@@ -201,24 +211,35 @@ public class UnitTrainingService
|
||||
return [];
|
||||
}
|
||||
|
||||
if (units.Any(x => x.MovementRemaining <= 0))
|
||||
{
|
||||
throw new InvalidOperationException("That squad has no movement remaining this tick.");
|
||||
}
|
||||
|
||||
var movementCompletesAt = now.AddMinutes(1);
|
||||
await _units.UpdateManyAsync(
|
||||
x => x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Status == "Idle" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, request.ToQ)
|
||||
.Set(x => x.R, request.ToR)
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.Status, "Moving")
|
||||
.Inc(x => x.MovementRemaining, -1)
|
||||
.Set(x => x.DestinationQ, request.ToQ)
|
||||
.Set(x => x.DestinationR, request.ToR)
|
||||
.Set(x => x.MovementStartedAtUtc, now)
|
||||
.Set(x => x.MovementCompletesAtUtc, movementCompletesAt)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
|
||||
await ClaimTargetIfEmptyAsync(username, request.ToQ, request.ToR, now);
|
||||
|
||||
return await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Q == request.ToQ &&
|
||||
x.R == request.ToR)
|
||||
x.Status == "Moving" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR &&
|
||||
x.DestinationQ == request.ToQ &&
|
||||
x.DestinationR == request.ToR)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -234,6 +255,86 @@ public class UnitTrainingService
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task CompletePendingMovementAsync(string username)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||
var movingUnits = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.Status == "Moving" &&
|
||||
x.MovementCompletesAtUtc != null &&
|
||||
x.MovementCompletesAtUtc <= now)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var unit in movingUnits)
|
||||
{
|
||||
if (unit.DestinationQ is null || unit.DestinationR is null)
|
||||
{
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.MovementPerTick, movementPerTick)
|
||||
.Set(x => x.MovementRemaining, movementPerTick)
|
||||
.Unset(x => x.MovementStartedAtUtc)
|
||||
.Unset(x => x.MovementCompletesAtUtc)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
continue;
|
||||
}
|
||||
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, unit.DestinationQ.Value)
|
||||
.Set(x => x.R, unit.DestinationR.Value)
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.MovementPerTick, movementPerTick)
|
||||
.Set(x => x.MovementRemaining, movementPerTick)
|
||||
.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));
|
||||
|
||||
if (unit.UnitType == ArcherUnitType)
|
||||
{
|
||||
await ClaimTargetIfEmptyAsync(username, unit.DestinationQ.Value, unit.DestinationR.Value, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefillMovementAsync(string username)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||
var units = await _units.Find(x => x.Username == username && x.Status != "Moving" && x.Status != "MovingToWork").ToListAsync();
|
||||
|
||||
foreach (var unit in units)
|
||||
{
|
||||
var lastRefill = unit.LastMovementRefillAtUtc.Year < 2000 ? now : unit.LastMovementRefillAtUtc;
|
||||
var elapsedTicks = (int)Math.Floor((now - lastRefill).TotalMinutes);
|
||||
var movementRemaining = elapsedTicks > 0
|
||||
? movementPerTick
|
||||
: Math.Min(unit.MovementRemaining, movementPerTick);
|
||||
if (elapsedTicks <= 0 && unit.MovementPerTick == movementPerTick && unit.MovementRemaining == movementRemaining)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var update = Builders<UnitInstance>.Update
|
||||
.Set(x => x.MovementPerTick, movementPerTick)
|
||||
.Set(x => x.MovementRemaining, movementRemaining)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
if (elapsedTicks > 0)
|
||||
{
|
||||
update = update.Set(x => x.LastMovementRefillAtUtc, lastRefill.AddMinutes(elapsedTicks));
|
||||
}
|
||||
|
||||
await _units.UpdateOneAsync(x => x.Id == unit.Id, update);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteFinishedBuildingsAsync(string username)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -376,6 +477,7 @@ public class UnitTrainingService
|
||||
continue;
|
||||
}
|
||||
|
||||
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||
var unit = new UnitInstance
|
||||
{
|
||||
Username = completed.Username,
|
||||
@@ -383,6 +485,9 @@ public class UnitTrainingService
|
||||
Q = completed.OriginQ ?? 0,
|
||||
R = completed.OriginR ?? 0,
|
||||
Status = "Idle",
|
||||
MovementPerTick = movementPerTick,
|
||||
MovementRemaining = movementPerTick,
|
||||
LastMovementRefillAtUtc = now,
|
||||
SourceTrainingId = completed.Id,
|
||||
CreatedAtUtc = now,
|
||||
UpdatedAtUtc = now
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"ScoutTrainingSeconds": 60,
|
||||
"PeasantGoldCost": 25,
|
||||
"PeasantTrainingSeconds": 60,
|
||||
"UnitFoodPerMinute": 1,
|
||||
"UnitMovementPerTick": 1,
|
||||
"ArcherGoldCost": 100,
|
||||
"ArcherWoodCost": 100,
|
||||
"ArcherTrainingSeconds": 60
|
||||
|
||||
@@ -21,6 +21,8 @@ data:
|
||||
Units__ScoutTrainingSeconds: "60"
|
||||
Units__PeasantGoldCost: "25"
|
||||
Units__PeasantTrainingSeconds: "60"
|
||||
Units__UnitFoodPerMinute: "1"
|
||||
Units__UnitMovementPerTick: "1"
|
||||
Units__ArcherGoldCost: "100"
|
||||
Units__ArcherWoodCost: "100"
|
||||
Units__ArcherTrainingSeconds: "60"
|
||||
|
||||
Reference in New Issue
Block a user