Files
territory-autobattler/microservices/LocationsApi/Services/ResourceService.cs
T
2026-05-20 04:09:32 -05:00

381 lines
16 KiB
C#

using LocationsApi.Models;
using MongoDB.Driver;
namespace LocationsApi.Services;
public class ResourceService
{
private readonly IMongoCollection<PlayerResources> _resources;
private readonly IMongoCollection<Location> _locations;
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;
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");
_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;
_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);
return await ApplyProductionBuildingDeliveryAsync(resources);
}
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)
{
var now = DateTime.UtcNow;
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 = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, 5);
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" || 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
});
}
}