using LocationsApi.Models; using MongoDB.Driver; namespace LocationsApi.Services; public class ResourceService { private readonly IMongoCollection _resources; private readonly int _startingGold; private readonly int _headquartersGoldPerMinute; public ResourceService(IConfiguration cfg) { 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"); _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; _resources.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(x => x.Username), new CreateIndexOptions { Unique = true })); } public async Task EnsureAsync(string username) { var now = DateTime.UtcNow; var update = Builders.Update .SetOnInsert(x => x.Username, username) .SetOnInsert(x => x.Gold, _startingGold) .SetOnInsert(x => x.Wood, 0) .SetOnInsert(x => x.Ore, 0) .SetOnInsert(x => x.CreatedAtUtc, now) .SetOnInsert(x => x.LastGoldGeneratedAtUtc, now) .SetOnInsert(x => x.LastWoodGeneratedAtUtc, now) .SetOnInsert(x => x.LastOreGeneratedAtUtc, now) .Set(x => x.GoldPerMinute, _headquartersGoldPerMinute) .Set(x => x.UpdatedAtUtc, now); var resources = await _resources.FindOneAndUpdateAsync( x => x.Username == username, update, new FindOneAndUpdateOptions { IsUpsert = true, ReturnDocument = ReturnDocument.After }); return await ApplyGoldGenerationAsync(resources); } public async Task GetAsync(string username) { return await EnsureAsync(username); } public async Task TrySpendGoldAsync(string username, int amount) { if (amount <= 0) { return true; } await EnsureAsync(username); var update = Builders.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 { ReturnDocument = ReturnDocument.After }); return updated is not null; } public async Task 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.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 { 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.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.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); } private async Task ApplyGoldGenerationAsync(PlayerResources resources) { var now = DateTime.UtcNow; var goldElapsedMinutes = resources.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); if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0) { return resources; } var update = Builders.Update .Set(x => x.GoldPerMinute, _headquartersGoldPerMinute) .Set(x => x.UpdatedAtUtc, now); if (goldElapsedMinutes > 0) { update = update .Inc(x => x.Gold, goldElapsedMinutes * resources.GoldPerMinute) .Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes)); } if (woodElapsedMinutes > 0) { update = update .Inc(x => x.Wood, woodElapsedMinutes * resources.WoodPerMinute) .Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes)); } if (oreElapsedMinutes > 0) { update = update .Inc(x => x.Ore, oreElapsedMinutes * resources.OrePerMinute) .Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes)); } return await _resources.FindOneAndUpdateAsync( x => x.Username == resources.Username, update, new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); } }