694 lines
28 KiB
C#
694 lines
28 KiB
C#
using LocationsApi.Models;
|
|
using MongoDB.Driver;
|
|
|
|
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;
|
|
private readonly int _townHallGoldCapacity;
|
|
private readonly int _townHallWoodCapacity;
|
|
private readonly int _townHallOreCapacity;
|
|
private readonly int _townHallFoodCapacity;
|
|
private readonly int _productionBuildingResourceCapacity;
|
|
private readonly int _unitFoodPerMinute;
|
|
|
|
public ResourceService(IConfiguration cfg, 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);
|
|
_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)
|
|
? Math.Max(0, goldPerMinute)
|
|
: 10;
|
|
_townHallGoldCapacity = int.TryParse(cfg["Resources:TownHallGoldCapacity"], out var goldCapacity)
|
|
? Math.Max(0, goldCapacity)
|
|
: 500;
|
|
_townHallWoodCapacity = int.TryParse(cfg["Resources:TownHallWoodCapacity"], out var woodCapacity)
|
|
? Math.Max(0, woodCapacity)
|
|
: 100;
|
|
_townHallOreCapacity = int.TryParse(cfg["Resources:TownHallOreCapacity"], out var oreCapacity)
|
|
? Math.Max(0, oreCapacity)
|
|
: 100;
|
|
_townHallFoodCapacity = int.TryParse(cfg["Resources:TownHallFoodCapacity"], out var foodCapacity)
|
|
? Math.Max(0, foodCapacity)
|
|
: 100;
|
|
_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),
|
|
new CreateIndexOptions { Unique = true }));
|
|
}
|
|
|
|
public async Task<PlayerResources> EnsureAsync(string username)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var startingGold = await _settings.GetNonNegativeIntAsync(GameSettingsService.StartingGold, _startingGold);
|
|
var goldPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.HeadquartersGoldPerMinute, _headquartersGoldPerMinute);
|
|
var goldCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallGoldCapacity, _townHallGoldCapacity);
|
|
var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity);
|
|
var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity);
|
|
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
|
var update = Builders<PlayerResources>.Update
|
|
.SetOnInsert(x => x.Username, username)
|
|
.SetOnInsert(x => x.Gold, Math.Min(startingGold, goldCapacity))
|
|
.SetOnInsert(x => x.Wood, 0)
|
|
.SetOnInsert(x => x.Ore, 0)
|
|
.SetOnInsert(x => x.Food, 0)
|
|
.SetOnInsert(x => x.CreatedAtUtc, now)
|
|
.SetOnInsert(x => x.LastGoldGeneratedAtUtc, now)
|
|
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
|
|
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
|
|
.SetOnInsert(x => x.LastFoodGeneratedAtUtc, now)
|
|
.Set(x => x.GoldPerMinute, goldPerMinute)
|
|
.Set(x => x.GoldCapacity, goldCapacity)
|
|
.Set(x => x.WoodCapacity, woodCapacity)
|
|
.Set(x => x.OreCapacity, oreCapacity)
|
|
.Set(x => x.FoodCapacity, foodCapacity)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
var resources = await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == username,
|
|
update,
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
IsUpsert = true,
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
|
|
return await ApplyResourceGenerationAsync(resources);
|
|
}
|
|
|
|
public async Task<PlayerResources> GetAsync(string username)
|
|
{
|
|
var resources = await EnsureAsync(username);
|
|
resources = await ApplyFoodTickAsync(resources);
|
|
return await ApplyProductionBuildingDeliveryAsync(resources, includeFood: false);
|
|
}
|
|
|
|
public async Task<bool> TrySpendGoldAsync(string username, int amount)
|
|
{
|
|
if (amount <= 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
await EnsureAsync(username);
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Inc(x => x.Gold, -amount)
|
|
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow);
|
|
|
|
var updated = await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == username && x.Gold >= amount,
|
|
update,
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
|
|
return updated is not null;
|
|
}
|
|
|
|
public async Task<bool> TrySpendResourcesAsync(string username, int gold, int wood, int ore)
|
|
{
|
|
gold = Math.Max(0, gold);
|
|
wood = Math.Max(0, wood);
|
|
ore = Math.Max(0, ore);
|
|
if (gold == 0 && wood == 0 && ore == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
await EnsureAsync(username);
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Inc(x => x.Gold, -gold)
|
|
.Inc(x => x.Wood, -wood)
|
|
.Inc(x => x.Ore, -ore)
|
|
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow);
|
|
|
|
var updated = await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == username && x.Gold >= gold && x.Wood >= wood && x.Ore >= ore,
|
|
update,
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
|
|
return updated is not null;
|
|
}
|
|
|
|
public async Task AddWoodGenerationAsync(string username, int woodPerMinute)
|
|
{
|
|
if (woodPerMinute <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var resources = await EnsureAsync(username);
|
|
var now = DateTime.UtcNow;
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Inc(x => x.WoodPerMinute, woodPerMinute)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000)
|
|
{
|
|
update = update.Set(x => x.LastWoodGeneratedAtUtc, now);
|
|
}
|
|
|
|
await _resources.UpdateOneAsync(x => x.Username == username, update);
|
|
}
|
|
|
|
public async Task AddOreGenerationAsync(string username, int orePerMinute)
|
|
{
|
|
if (orePerMinute <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var resources = await EnsureAsync(username);
|
|
var now = DateTime.UtcNow;
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Inc(x => x.OrePerMinute, orePerMinute)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000)
|
|
{
|
|
update = update.Set(x => x.LastOreGeneratedAtUtc, now);
|
|
}
|
|
|
|
await _resources.UpdateOneAsync(x => x.Username == username, update);
|
|
}
|
|
|
|
public async Task AddFoodGenerationAsync(string username, int foodPerMinute)
|
|
{
|
|
if (foodPerMinute <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var resources = await EnsureAsync(username);
|
|
var now = DateTime.UtcNow;
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Inc(x => x.FoodPerMinute, foodPerMinute)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (resources.FoodPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000)
|
|
{
|
|
update = update.Set(x => x.LastFoodGeneratedAtUtc, now);
|
|
}
|
|
|
|
await _resources.UpdateOneAsync(x => x.Username == username, update);
|
|
}
|
|
|
|
public async Task SetBuildingGenerationAsync(string username, int woodPerMinute, int orePerMinute, int foodPerMinute)
|
|
{
|
|
await EnsureAsync(username);
|
|
var now = DateTime.UtcNow;
|
|
var update = Builders<PlayerResources>.Update
|
|
.Set(x => x.WoodPerMinute, Math.Max(0, woodPerMinute))
|
|
.Set(x => x.OrePerMinute, Math.Max(0, orePerMinute))
|
|
.Set(x => x.FoodPerMinute, Math.Max(0, foodPerMinute))
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (woodPerMinute > 0)
|
|
{
|
|
update = update.Set(x => x.LastWoodGeneratedAtUtc, now);
|
|
}
|
|
if (orePerMinute > 0)
|
|
{
|
|
update = update.Set(x => x.LastOreGeneratedAtUtc, now);
|
|
}
|
|
if (foodPerMinute > 0)
|
|
{
|
|
update = update.Set(x => x.LastFoodGeneratedAtUtc, now);
|
|
}
|
|
|
|
await _resources.UpdateOneAsync(x => x.Username == username, update);
|
|
}
|
|
|
|
|
|
private async Task<PlayerResources> ApplyResourceGenerationAsync(PlayerResources resources)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var goldPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.HeadquartersGoldPerMinute, _headquartersGoldPerMinute);
|
|
var goldCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallGoldCapacity, _townHallGoldCapacity);
|
|
var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity);
|
|
var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity);
|
|
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
|
var goldElapsedMinutes = goldPerMinute <= 0
|
|
? 0
|
|
: (int)Math.Floor((now - resources.LastGoldGeneratedAtUtc).TotalMinutes);
|
|
var cappedGold = Math.Min(resources.Gold, goldCapacity);
|
|
var cappedWood = Math.Min(resources.Wood, woodCapacity);
|
|
var cappedOre = Math.Min(resources.Ore, oreCapacity);
|
|
var cappedFood = Math.Min(resources.Food, foodCapacity);
|
|
var isOverCapacity = resources.Gold != cappedGold || resources.Wood != cappedWood || resources.Ore != cappedOre || resources.Food != cappedFood;
|
|
|
|
if (goldElapsedMinutes <= 0 && !isOverCapacity)
|
|
{
|
|
return resources;
|
|
}
|
|
|
|
var update = Builders<PlayerResources>.Update
|
|
.Set(x => x.GoldPerMinute, goldPerMinute)
|
|
.Set(x => x.GoldCapacity, goldCapacity)
|
|
.Set(x => x.WoodCapacity, woodCapacity)
|
|
.Set(x => x.OreCapacity, oreCapacity)
|
|
.Set(x => x.FoodCapacity, foodCapacity)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (isOverCapacity)
|
|
{
|
|
update = update
|
|
.Set(x => x.Gold, cappedGold)
|
|
.Set(x => x.Wood, cappedWood)
|
|
.Set(x => x.Ore, cappedOre)
|
|
.Set(x => x.Food, cappedFood);
|
|
}
|
|
|
|
if (goldElapsedMinutes > 0)
|
|
{
|
|
var generatedGold = Math.Min(goldCapacity, cappedGold + (goldElapsedMinutes * goldPerMinute));
|
|
update = update
|
|
.Set(x => x.Gold, generatedGold)
|
|
.Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes));
|
|
}
|
|
|
|
return await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == resources.Username,
|
|
update,
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
}
|
|
|
|
private async Task<PlayerResources> ApplyProductionBuildingDeliveryAsync(PlayerResources resources, bool includeFood = true)
|
|
{
|
|
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);
|
|
var buildingCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.ProductionBuildingResourceCapacity, _productionBuildingResourceCapacity);
|
|
var logCampWoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.LogCampWoodPerMinute, 5);
|
|
var mineOrePerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.MineOrePerMinute, 5);
|
|
var farmFoodPerMinute = includeFood
|
|
? await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, 5)
|
|
: 0;
|
|
|
|
var buildings = await _locations.Find(x =>
|
|
x.OwnerUsername == resources.Username &&
|
|
!string.IsNullOrWhiteSpace(x.WorkerUnitId) &&
|
|
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") &&
|
|
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || (includeFood && x.BuildingType == "Farm")))
|
|
.ToListAsync();
|
|
|
|
var wood = Math.Min(resources.Wood, woodCapacity);
|
|
var ore = Math.Min(resources.Ore, oreCapacity);
|
|
var food = Math.Min(resources.Food, foodCapacity);
|
|
|
|
foreach (var building in buildings)
|
|
{
|
|
var rate = building.BuildingType switch
|
|
{
|
|
"LogCamp" => logCampWoodPerMinute,
|
|
"Mine" => mineOrePerMinute,
|
|
"Farm" => farmFoodPerMinute,
|
|
_ => 0
|
|
};
|
|
var lastGenerated = building.LastResourceGeneratedAtUtc ?? building.BuildingBuiltAtUtc ?? now;
|
|
var elapsedMinutes = rate <= 0 ? 0 : (int)Math.Floor((now - lastGenerated).TotalMinutes);
|
|
var stored = Math.Min(buildingCapacity, Math.Max(0, building.StoredResourceAmount) + (elapsedMinutes * rate));
|
|
|
|
var delivered = 0;
|
|
if (building.BuildingType == "LogCamp")
|
|
{
|
|
delivered = Math.Min(stored, Math.Max(0, woodCapacity - wood));
|
|
wood += delivered;
|
|
}
|
|
else if (building.BuildingType == "Mine")
|
|
{
|
|
delivered = Math.Min(stored, Math.Max(0, oreCapacity - ore));
|
|
ore += delivered;
|
|
}
|
|
else if (building.BuildingType == "Farm")
|
|
{
|
|
delivered = Math.Min(stored, Math.Max(0, foodCapacity - food));
|
|
food += delivered;
|
|
}
|
|
|
|
var update = Builders<Location>.Update
|
|
.Set(x => x.StoredResourceAmount, stored - delivered)
|
|
.Set(x => x.StoredResourceCapacity, buildingCapacity)
|
|
.Set(x => x.UpdatedAtUtc, now);
|
|
|
|
if (elapsedMinutes > 0 || building.LastResourceGeneratedAtUtc is null)
|
|
{
|
|
update = update.Set(x => x.LastResourceGeneratedAtUtc, lastGenerated.AddMinutes(elapsedMinutes));
|
|
}
|
|
|
|
await _locations.UpdateOneAsync(x => x.Id == building.Id, update);
|
|
}
|
|
|
|
return await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == resources.Username,
|
|
Builders<PlayerResources>.Update
|
|
.Set(x => x.Wood, wood)
|
|
.Set(x => x.Ore, ore)
|
|
.Set(x => x.Food, food)
|
|
.Set(x => x.WoodCapacity, woodCapacity)
|
|
.Set(x => x.OreCapacity, oreCapacity)
|
|
.Set(x => x.FoodCapacity, foodCapacity)
|
|
.Set(x => x.UpdatedAtUtc, now),
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
}
|
|
|
|
private async Task<PlayerResources> ApplyFoodTickAsync(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 = resources.LastFoodGeneratedAtUtc.Year < 2000
|
|
? 0
|
|
: (int)Math.Floor((now - resources.LastFoodGeneratedAtUtc).TotalMinutes);
|
|
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
|
var food = Math.Min(resources.Food, foodCapacity);
|
|
var removedUnits = new List<FoodConsumingUnit>();
|
|
|
|
for (var minute = 0; minute < elapsedMinutes; minute++)
|
|
{
|
|
if (foodProductionPerMinute > 0)
|
|
{
|
|
food = Math.Min(foodCapacity, food + foodProductionPerMinute);
|
|
}
|
|
|
|
var neededFood = units.Count * unitFoodPerMinute;
|
|
if (neededFood <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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 && resources.FoodCapacity == foodCapacity)
|
|
{
|
|
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.FoodCapacity, foodCapacity)
|
|
.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 == ScoutUnitType || 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;
|
|
if (HexDistance(worker.Q, worker.R, destinationQ, destinationR) > 1)
|
|
{
|
|
var nextStep = await NextPeasantStepTowardAsync(username, worker.Q, worker.R, destinationQ, destinationR);
|
|
if (nextStep is null)
|
|
{
|
|
await _units.UpdateOneAsync(
|
|
x => x.Id == worker.Id,
|
|
Builders<UnitInstance>.Update
|
|
.Set(x => x.Status, "Idle")
|
|
.Unset(x => x.DestinationQ)
|
|
.Unset(x => x.DestinationR)
|
|
.Unset(x => x.MovementStartedAtUtc)
|
|
.Unset(x => x.MovementCompletesAtUtc)
|
|
.Set(x => x.UpdatedAtUtc, now));
|
|
continue;
|
|
}
|
|
|
|
await _units.UpdateOneAsync(
|
|
x => x.Id == worker.Id,
|
|
Builders<UnitInstance>.Update
|
|
.Set(x => x.Q, nextStep.Value.q)
|
|
.Set(x => x.R, nextStep.Value.r)
|
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
|
.Set(x => x.UpdatedAtUtc, now));
|
|
continue;
|
|
}
|
|
|
|
var location = await _locations.FindOneAndUpdateAsync(
|
|
x => x.OwnerUsername == username &&
|
|
x.Q == destinationQ &&
|
|
x.R == destinationR &&
|
|
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Farm") &&
|
|
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 async Task<(int q, int r)?> NextPeasantStepTowardAsync(string username, int fromQ, int fromR, int toQ, int toR)
|
|
{
|
|
var owned = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
|
var ownedCoordinates = owned.Select(x => (x.Q, x.R)).ToHashSet();
|
|
if (!ownedCoordinates.Contains((toQ, toR)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var start = (fromQ, fromR);
|
|
var target = (toQ, toR);
|
|
var queue = new Queue<(int q, int r)>();
|
|
var previous = new Dictionary<(int q, int r), (int q, int r)>();
|
|
var visited = new HashSet<(int q, int r)> { start };
|
|
queue.Enqueue(start);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (current == target)
|
|
{
|
|
break;
|
|
}
|
|
|
|
foreach (var neighbor in NeighborCoordinates(current.q, current.r)
|
|
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
|
.ThenBy(x => x.q)
|
|
.ThenBy(x => x.r))
|
|
{
|
|
if (!ownedCoordinates.Contains(neighbor) || !visited.Add(neighbor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
previous[neighbor] = current;
|
|
queue.Enqueue(neighbor);
|
|
}
|
|
}
|
|
|
|
if (!visited.Contains(target))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var step = target;
|
|
while (previous.TryGetValue(step, out var before) && before != start)
|
|
{
|
|
step = before;
|
|
}
|
|
|
|
return step == start ? target : step;
|
|
}
|
|
|
|
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);
|
|
}
|