299 lines
12 KiB
C#
299 lines
12 KiB
C#
using LocationsApi.Models;
|
|
using MongoDB.Driver;
|
|
|
|
namespace LocationsApi.Services;
|
|
|
|
public class ResourceService
|
|
{
|
|
private readonly IMongoCollection<PlayerResources> _resources;
|
|
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;
|
|
|
|
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");
|
|
_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;
|
|
|
|
_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)
|
|
{
|
|
return await EnsureAsync(username);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000
|
|
? 0
|
|
: (int)Math.Floor((now - resources.LastWoodGeneratedAtUtc).TotalMinutes);
|
|
var oreElapsedMinutes = resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000
|
|
? 0
|
|
: (int)Math.Floor((now - resources.LastOreGeneratedAtUtc).TotalMinutes);
|
|
var foodElapsedMinutes = resources.FoodPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000
|
|
? 0
|
|
: (int)Math.Floor((now - resources.LastFoodGeneratedAtUtc).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 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0 && foodElapsedMinutes <= 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));
|
|
}
|
|
|
|
if (woodElapsedMinutes > 0)
|
|
{
|
|
var generatedWood = Math.Min(woodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute));
|
|
update = update
|
|
.Set(x => x.Wood, generatedWood)
|
|
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
|
|
}
|
|
|
|
if (oreElapsedMinutes > 0)
|
|
{
|
|
var generatedOre = Math.Min(oreCapacity, cappedOre + (oreElapsedMinutes * resources.OrePerMinute));
|
|
update = update
|
|
.Set(x => x.Ore, generatedOre)
|
|
.Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes));
|
|
}
|
|
|
|
if (foodElapsedMinutes > 0)
|
|
{
|
|
var generatedFood = Math.Min(foodCapacity, cappedFood + (foodElapsedMinutes * resources.FoodPerMinute));
|
|
update = update
|
|
.Set(x => x.Food, generatedFood)
|
|
.Set(x => x.LastFoodGeneratedAtUtc, resources.LastFoodGeneratedAtUtc.AddMinutes(foodElapsedMinutes));
|
|
}
|
|
|
|
return await _resources.FindOneAndUpdateAsync(
|
|
x => x.Username == resources.Username,
|
|
update,
|
|
new FindOneAndUpdateOptions<PlayerResources>
|
|
{
|
|
ReturnDocument = ReturnDocument.After
|
|
});
|
|
}
|
|
}
|