Adding barracks
This commit is contained in:
@@ -88,6 +88,11 @@ public class LocationsController : ControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!User.IsInRole("SUPER") && !await _locations.IsAdjacentToOwnedAsync(username, request.Q, request.R))
|
||||
{
|
||||
return BadRequest("Territory claims must be adjacent to controlled territory.");
|
||||
}
|
||||
|
||||
var existingLocation = await _locations.GetAsync(request.Q, request.R);
|
||||
if (existingLocation is not null && !string.IsNullOrWhiteSpace(existingLocation.OwnerUsername))
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ public class LocationResponse
|
||||
Label = location.Label,
|
||||
Level = location.Level,
|
||||
BuildingType = location.BuildingType,
|
||||
BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
? "Built"
|
||||
: location.BuildingStatus,
|
||||
BuildingStartedAtUtc = location.BuildingStartedAtUtc,
|
||||
|
||||
@@ -44,8 +44,10 @@ training record.
|
||||
Buildings are stored on location records. `LogCamp` can be built on controlled
|
||||
Forest territory, takes 60 seconds to build, and adds 5 wood per minute after it
|
||||
finishes. `Mine` can be built on controlled Ore territory, takes 60 seconds to
|
||||
build, and adds 5 ore per minute after it finishes. Each location has one
|
||||
building slot; a building under construction occupies that slot.
|
||||
build, and adds 5 ore per minute after it finishes. `Barracks` can be built on
|
||||
any controlled territory, costs 500 gold, 100 wood, and 100 ore, and takes 60
|
||||
seconds to build. Each location has one building slot; a building under
|
||||
construction occupies that slot.
|
||||
|
||||
Timer settings are stored in the MongoDB `GameSettings` collection and are
|
||||
seeded on first read:
|
||||
@@ -53,3 +55,4 @@ seeded on first read:
|
||||
- `ScoutTrainingSeconds`
|
||||
- `LogCampBuildSeconds`
|
||||
- `MineBuildSeconds`
|
||||
- `BarracksBuildSeconds`
|
||||
|
||||
@@ -8,6 +8,7 @@ public class GameSettingsService
|
||||
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
|
||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
||||
|
||||
private readonly IMongoCollection<GameSetting> _settings;
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ public class LocationService
|
||||
private readonly int _mineOrePerMinute;
|
||||
private readonly int _defaultLogCampBuildSeconds;
|
||||
private readonly int _defaultMineBuildSeconds;
|
||||
private readonly int _defaultBarracksBuildSeconds;
|
||||
private readonly int _barracksGoldCost;
|
||||
private readonly int _barracksWoodCost;
|
||||
private readonly int _barracksOreCost;
|
||||
|
||||
public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||
{
|
||||
@@ -40,6 +44,18 @@ public class LocationService
|
||||
_defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds)
|
||||
? Math.Max(1, mineBuildSeconds)
|
||||
: 60;
|
||||
_defaultBarracksBuildSeconds = int.TryParse(cfg["Buildings:BarracksBuildSeconds"], out var barracksBuildSeconds)
|
||||
? Math.Max(1, barracksBuildSeconds)
|
||||
: 60;
|
||||
_barracksGoldCost = int.TryParse(cfg["Buildings:BarracksGoldCost"], out var barracksGoldCost)
|
||||
? Math.Max(0, barracksGoldCost)
|
||||
: 500;
|
||||
_barracksWoodCost = int.TryParse(cfg["Buildings:BarracksWoodCost"], out var barracksWoodCost)
|
||||
? Math.Max(0, barracksWoodCost)
|
||||
: 100;
|
||||
_barracksOreCost = int.TryParse(cfg["Buildings:BarracksOreCost"], out var barracksOreCost)
|
||||
? Math.Max(0, barracksOreCost)
|
||||
: 100;
|
||||
|
||||
var coordinateIndex = Builders<Location>.IndexKeys
|
||||
.Ascending(x => x.Q)
|
||||
@@ -149,8 +165,8 @@ public class LocationService
|
||||
throw new InvalidOperationException("This location already has a building or construction in progress.");
|
||||
}
|
||||
|
||||
var requiredTerrain = buildingType == "LogCamp" ? "Forest" : "Ore";
|
||||
if (!string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
|
||||
var requiredTerrain = RequiredTerrainForBuilding(buildingType);
|
||||
if (requiredTerrain is not null && !string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory.");
|
||||
}
|
||||
@@ -158,12 +174,8 @@ public class LocationService
|
||||
await CompleteFinishedBuildingsAsync(username);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var settingKey = buildingType == "LogCamp"
|
||||
? GameSettingsService.LogCampBuildSeconds
|
||||
: GameSettingsService.MineBuildSeconds;
|
||||
var defaultBuildSeconds = buildingType == "LogCamp"
|
||||
? _defaultLogCampBuildSeconds
|
||||
: _defaultMineBuildSeconds;
|
||||
var settingKey = BuildSecondsSettingKey(buildingType);
|
||||
var defaultBuildSeconds = DefaultBuildSeconds(buildingType);
|
||||
var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds);
|
||||
var update = Builders<Location>.Update
|
||||
.Set(x => x.BuildingType, buildingType)
|
||||
@@ -173,7 +185,7 @@ public class LocationService
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
var updated = await _locations.FindOneAndUpdateAsync(
|
||||
x => x.Id == location.Id && x.OwnerUsername == username && x.BuildingType == null,
|
||||
x => x.Id == location.Id && x.OwnerUsername == username && (x.BuildingType == null || x.BuildingType == ""),
|
||||
update,
|
||||
new FindOneAndUpdateOptions<Location>
|
||||
{
|
||||
@@ -185,6 +197,27 @@ public class LocationService
|
||||
throw new InvalidOperationException("This location already has a building or construction in progress.");
|
||||
}
|
||||
|
||||
if (buildingType == "Barracks")
|
||||
{
|
||||
var spent = await _resources.TrySpendResourcesAsync(username, _barracksGoldCost, _barracksWoodCost, _barracksOreCost);
|
||||
if (!spent)
|
||||
{
|
||||
var rollback = Builders<Location>.Update
|
||||
.Unset(x => x.BuildingType)
|
||||
.Unset(x => x.BuildingStatus)
|
||||
.Unset(x => x.BuildingStartedAtUtc)
|
||||
.Unset(x => x.BuildingCompletesAtUtc)
|
||||
.Unset(x => x.BuildingBuiltAtUtc)
|
||||
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow);
|
||||
|
||||
await _locations.UpdateOneAsync(
|
||||
x => x.Id == updated.Id && x.BuildingType == "Barracks" && x.BuildingStatus == "Building" && x.BuildingStartedAtUtc == now,
|
||||
rollback);
|
||||
|
||||
throw new InvalidOperationException($"Not enough resources to build Barracks. Required: {_barracksGoldCost} gold, {_barracksWoodCost} wood, {_barracksOreCost} ore.");
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -193,7 +226,7 @@ public class LocationService
|
||||
var now = DateTime.UtcNow;
|
||||
var readyBuildings = await _locations.Find(x =>
|
||||
x.OwnerUsername == username &&
|
||||
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine") &&
|
||||
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Barracks") &&
|
||||
x.BuildingStatus == "Building" &&
|
||||
x.BuildingCompletesAtUtc <= now)
|
||||
.ToListAsync();
|
||||
@@ -353,6 +386,15 @@ public class LocationService
|
||||
return ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth);
|
||||
}
|
||||
|
||||
public async Task<bool> IsAdjacentToOwnedAsync(string username, int q, int r)
|
||||
{
|
||||
var neighbors = NeighborCoordinates(q, r).ToList();
|
||||
return await _locations.Find(x =>
|
||||
x.OwnerUsername == username &&
|
||||
neighbors.Any(n => n.Q == x.Q && n.R == x.R))
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
private async Task<Location?> TryCreateHomeAsync(string username, int q, int r)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -582,7 +624,35 @@ public class LocationService
|
||||
{
|
||||
"logcamp" or "log camp" => "LogCamp",
|
||||
"mine" => "Mine",
|
||||
"barracks" => "Barracks",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string? RequiredTerrainForBuilding(string buildingType) =>
|
||||
buildingType switch
|
||||
{
|
||||
"LogCamp" => "Forest",
|
||||
"Mine" => "Ore",
|
||||
"Barracks" => null,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string BuildSecondsSettingKey(string buildingType) =>
|
||||
buildingType switch
|
||||
{
|
||||
"LogCamp" => GameSettingsService.LogCampBuildSeconds,
|
||||
"Mine" => GameSettingsService.MineBuildSeconds,
|
||||
"Barracks" => GameSettingsService.BarracksBuildSeconds,
|
||||
_ => GameSettingsService.LogCampBuildSeconds
|
||||
};
|
||||
|
||||
private int DefaultBuildSeconds(string buildingType) =>
|
||||
buildingType switch
|
||||
{
|
||||
"LogCamp" => _defaultLogCampBuildSeconds,
|
||||
"Mine" => _defaultMineBuildSeconds,
|
||||
"Barracks" => _defaultBarracksBuildSeconds,
|
||||
_ => 60
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,35 @@ public class ResourceService
|
||||
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)
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
"LogCampWoodPerMinute": 5,
|
||||
"LogCampBuildSeconds": 60,
|
||||
"MineOrePerMinute": 5,
|
||||
"MineBuildSeconds": 60
|
||||
"MineBuildSeconds": 60,
|
||||
"BarracksBuildSeconds": 60,
|
||||
"BarracksGoldCost": 500,
|
||||
"BarracksWoodCost": 100,
|
||||
"BarracksOreCost": 100
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -19,3 +19,7 @@ data:
|
||||
Buildings__LogCampBuildSeconds: "60"
|
||||
Buildings__MineOrePerMinute: "5"
|
||||
Buildings__MineBuildSeconds: "60"
|
||||
Buildings__BarracksBuildSeconds: "60"
|
||||
Buildings__BarracksGoldCost: "500"
|
||||
Buildings__BarracksWoodCost: "100"
|
||||
Buildings__BarracksOreCost: "100"
|
||||
|
||||
Reference in New Issue
Block a user