adding resource limits and archers

This commit is contained in:
hz
2026-05-19 05:11:57 -05:00
parent a5ecb7f585
commit e78eb24543
12 changed files with 241 additions and 39 deletions
@@ -6,6 +6,7 @@ namespace LocationsApi.Services;
public class GameSettingsService
{
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
public const string ArcherTrainingSeconds = "ArcherTrainingSeconds";
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
public const string MineBuildSeconds = "MineBuildSeconds";
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
@@ -8,6 +8,9 @@ public class ResourceService
private readonly IMongoCollection<PlayerResources> _resources;
private readonly int _startingGold;
private readonly int _headquartersGoldPerMinute;
private readonly int _townHallGoldCapacity;
private readonly int _townHallWoodCapacity;
private readonly int _townHallOreCapacity;
public ResourceService(IConfiguration cfg)
{
@@ -20,6 +23,15 @@ public class ResourceService
_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;
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
@@ -31,7 +43,7 @@ public class ResourceService
var now = DateTime.UtcNow;
var update = Builders<PlayerResources>.Update
.SetOnInsert(x => x.Username, username)
.SetOnInsert(x => x.Gold, _startingGold)
.SetOnInsert(x => x.Gold, Math.Min(_startingGold, _townHallGoldCapacity))
.SetOnInsert(x => x.Wood, 0)
.SetOnInsert(x => x.Ore, 0)
.SetOnInsert(x => x.CreatedAtUtc, now)
@@ -39,6 +51,9 @@ public class ResourceService
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
.Set(x => x.GoldCapacity, _townHallGoldCapacity)
.Set(x => x.WoodCapacity, _townHallWoodCapacity)
.Set(x => x.OreCapacity, _townHallOreCapacity)
.Set(x => x.UpdatedAtUtc, now);
var resources = await _resources.FindOneAndUpdateAsync(
@@ -167,34 +182,52 @@ public class ResourceService
var oreElapsedMinutes = resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000
? 0
: (int)Math.Floor((now - resources.LastOreGeneratedAtUtc).TotalMinutes);
var cappedGold = Math.Min(resources.Gold, _townHallGoldCapacity);
var cappedWood = Math.Min(resources.Wood, _townHallWoodCapacity);
var cappedOre = Math.Min(resources.Ore, _townHallOreCapacity);
var isOverCapacity = resources.Gold != cappedGold || resources.Wood != cappedWood || resources.Ore != cappedOre;
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0)
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0 && !isOverCapacity)
{
return resources;
}
var update = Builders<PlayerResources>.Update
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
.Set(x => x.GoldCapacity, _townHallGoldCapacity)
.Set(x => x.WoodCapacity, _townHallWoodCapacity)
.Set(x => x.OreCapacity, _townHallOreCapacity)
.Set(x => x.UpdatedAtUtc, now);
if (isOverCapacity)
{
update = update
.Set(x => x.Gold, cappedGold)
.Set(x => x.Wood, cappedWood)
.Set(x => x.Ore, cappedOre);
}
if (goldElapsedMinutes > 0)
{
var generatedGold = Math.Min(_townHallGoldCapacity, cappedGold + (goldElapsedMinutes * resources.GoldPerMinute));
update = update
.Inc(x => x.Gold, goldElapsedMinutes * resources.GoldPerMinute)
.Set(x => x.Gold, generatedGold)
.Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes));
}
if (woodElapsedMinutes > 0)
{
var generatedWood = Math.Min(_townHallWoodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute));
update = update
.Inc(x => x.Wood, woodElapsedMinutes * resources.WoodPerMinute)
.Set(x => x.Wood, generatedWood)
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
}
if (oreElapsedMinutes > 0)
{
var generatedOre = Math.Min(_townHallOreCapacity, cappedOre + (oreElapsedMinutes * resources.OrePerMinute));
update = update
.Inc(x => x.Ore, oreElapsedMinutes * resources.OrePerMinute)
.Set(x => x.Ore, generatedOre)
.Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes));
}
@@ -6,12 +6,17 @@ namespace LocationsApi.Services;
public class UnitTrainingService
{
private const string ScoutUnitType = "Scout";
private const string ArcherUnitType = "Archer";
private readonly IMongoCollection<UnitTraining> _training;
private readonly IMongoCollection<Location> _locations;
private readonly ResourceService _resources;
private readonly GameSettingsService _settings;
private readonly int _scoutGoldCost;
private readonly int _archerGoldCost;
private readonly int _archerWoodCost;
private readonly int _defaultScoutTrainingSeconds;
private readonly int _defaultArcherTrainingSeconds;
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
{
@@ -20,12 +25,18 @@ public class UnitTrainingService
var client = new MongoClient(connectionString);
var database = client.GetDatabase(databaseName);
_training = database.GetCollection<UnitTraining>("UnitTraining");
_locations = database.GetCollection<Location>("Locations");
_resources = resources;
_settings = settings;
_scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50;
_archerGoldCost = int.TryParse(cfg["Units:ArcherGoldCost"], out var archerGoldCost) ? Math.Max(0, archerGoldCost) : 100;
_archerWoodCost = int.TryParse(cfg["Units:ArcherWoodCost"], out var archerWoodCost) ? Math.Max(0, archerWoodCost) : 100;
_defaultScoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds)
? Math.Max(1, scoutTrainingSeconds)
: 60;
_defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds)
? Math.Max(1, archerTrainingSeconds)
: 60;
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
@@ -38,6 +49,7 @@ public class UnitTrainingService
var all = await _training.Find(x => x.Username == username).ToListAsync();
var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
var completedArchers = all.Count(x => x.UnitType == ArcherUnitType && x.Status == "Completed");
var activeTraining = all
.Where(x => x.Status == "Training")
.OrderBy(x => x.CompletesAtUtc)
@@ -47,33 +59,47 @@ public class UnitTrainingService
return new PlayerUnitsResponse
{
CompletedScouts = completedScouts,
CompletedArchers = completedArchers,
Training = activeTraining
};
}
public async Task<UnitTraining?> TrainAsync(string username, string unitType)
{
if (!string.Equals(unitType, ScoutUnitType, StringComparison.OrdinalIgnoreCase))
var normalizedUnitType = NormalizeUnitType(unitType);
if (normalizedUnitType is null)
{
throw new ArgumentException("Unsupported unit type.", nameof(unitType));
}
var spent = await _resources.TrySpendGoldAsync(username, _scoutGoldCost);
if (normalizedUnitType == ArcherUnitType && !await HasBuiltBarracksAsync(username))
{
throw new InvalidOperationException("A completed Barracks is required to train Archers.");
}
var goldCost = normalizedUnitType == ArcherUnitType ? _archerGoldCost : _scoutGoldCost;
var woodCost = normalizedUnitType == ArcherUnitType ? _archerWoodCost : 0;
var spent = normalizedUnitType == ArcherUnitType
? await _resources.TrySpendResourcesAsync(username, goldCost, woodCost, 0)
: await _resources.TrySpendGoldAsync(username, goldCost);
if (!spent)
{
return null;
}
var now = DateTime.UtcNow;
var scoutTrainingSeconds = await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds);
var trainingSeconds = normalizedUnitType == ArcherUnitType
? await _settings.GetIntAsync(GameSettingsService.ArcherTrainingSeconds, _defaultArcherTrainingSeconds)
: await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds);
var training = new UnitTraining
{
Username = username,
UnitType = ScoutUnitType,
GoldCost = _scoutGoldCost,
UnitType = normalizedUnitType,
GoldCost = goldCost,
WoodCost = woodCost,
Status = "Training",
StartedAtUtc = now,
CompletesAtUtc = now.AddSeconds(scoutTrainingSeconds),
CompletesAtUtc = now.AddSeconds(trainingSeconds),
CreatedAtUtc = now,
UpdatedAtUtc = now
};
@@ -82,6 +108,32 @@ public class UnitTrainingService
return training;
}
private async Task<bool> HasBuiltBarracksAsync(string username)
{
await CompleteFinishedBuildingsAsync(username);
return await _locations.Find(x =>
x.OwnerUsername == username &&
x.BuildingType == "Barracks" &&
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == ""))
.AnyAsync();
}
private async Task CompleteFinishedBuildingsAsync(string username)
{
var now = DateTime.UtcNow;
var update = Builders<Location>.Update
.Set(x => x.BuildingStatus, "Built")
.Set(x => x.BuildingBuiltAtUtc, now)
.Set(x => x.UpdatedAtUtc, now);
await _locations.UpdateManyAsync(
x => x.OwnerUsername == username &&
x.BuildingType == "Barracks" &&
x.BuildingStatus == "Building" &&
x.BuildingCompletesAtUtc <= now,
update);
}
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
{
await CompleteFinishedTrainingAsync(username);
@@ -114,4 +166,15 @@ public class UnitTrainingService
x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now,
update);
}
private static string? NormalizeUnitType(string unitType)
{
var trimmed = unitType.Trim();
return trimmed.ToLowerInvariant() switch
{
"scout" => ScoutUnitType,
"archer" => ArcherUnitType,
_ => null
};
}
}