adding mines
This commit is contained in:
@@ -52,7 +52,7 @@ public class LocationResponse
|
||||
Label = location.Label,
|
||||
Level = location.Level,
|
||||
BuildingType = location.BuildingType,
|
||||
BuildingStatus = location.BuildingType == "LogCamp" && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
? "Built"
|
||||
: location.BuildingStatus,
|
||||
BuildingStartedAtUtc = location.BuildingStartedAtUtc,
|
||||
|
||||
@@ -14,8 +14,11 @@ public class PlayerResources
|
||||
public int GoldPerMinute { get; set; } = 10;
|
||||
public int Wood { get; set; }
|
||||
public int WoodPerMinute { get; set; }
|
||||
public int Ore { get; set; }
|
||||
public int OrePerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastOreGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ public class PlayerResourcesResponse
|
||||
public int GoldPerMinute { get; set; }
|
||||
public int Wood { get; set; }
|
||||
public int WoodPerMinute { get; set; }
|
||||
public int Ore { get; set; }
|
||||
public int OrePerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
||||
public DateTime LastWoodGeneratedAtUtc { get; set; }
|
||||
public DateTime LastOreGeneratedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
public static PlayerResourcesResponse FromResources(PlayerResources resources) => new()
|
||||
@@ -18,8 +21,11 @@ public class PlayerResourcesResponse
|
||||
GoldPerMinute = resources.GoldPerMinute,
|
||||
Wood = resources.Wood,
|
||||
WoodPerMinute = resources.WoodPerMinute,
|
||||
Ore = resources.Ore,
|
||||
OrePerMinute = resources.OrePerMinute,
|
||||
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
||||
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
|
||||
LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc,
|
||||
UpdatedAtUtc = resources.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,26 +28,28 @@ locations are unowned `Open` records until claimed by a player.
|
||||
|
||||
Player resource state is stored server-side in the `PlayerResources` collection.
|
||||
New players start with 100 gold. A player's headquarters generates 10 gold per
|
||||
minute. Log Camps generate wood. Generation is applied lazily when the resource
|
||||
record is read.
|
||||
minute. Log Camps generate wood and Mines generate ore. Generation is applied
|
||||
lazily when the resource record is read.
|
||||
|
||||
## Units
|
||||
|
||||
Unit training is stored server-side in the `UnitTraining` collection. The first
|
||||
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
|
||||
Finished training is completed lazily when unit state is read. Claiming an
|
||||
unclaimed visible location consumes one completed Scout.
|
||||
unclaimed visible location consumes one completed Scout by deleting that Scout's
|
||||
training record.
|
||||
|
||||
## Buildings
|
||||
|
||||
Buildings are stored on location records. The first building type is `LogCamp`,
|
||||
which can be built on controlled Forest territory, takes 60 seconds to build,
|
||||
and adds 5 wood per minute to the owning player's resource generation after it
|
||||
finishes. Each location has one building slot; a building under construction
|
||||
occupies that slot.
|
||||
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.
|
||||
|
||||
Timer settings are stored in the MongoDB `GameSettings` collection and are
|
||||
seeded on first read:
|
||||
|
||||
- `ScoutTrainingSeconds`
|
||||
- `LogCampBuildSeconds`
|
||||
- `MineBuildSeconds`
|
||||
|
||||
@@ -7,6 +7,7 @@ public class GameSettingsService
|
||||
{
|
||||
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
|
||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||
|
||||
private readonly IMongoCollection<GameSetting> _settings;
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ public class LocationService
|
||||
private readonly int _homeSearchRadius;
|
||||
private readonly int _visibilityDepth;
|
||||
private readonly int _logCampWoodPerMinute;
|
||||
private readonly int _mineOrePerMinute;
|
||||
private readonly int _defaultLogCampBuildSeconds;
|
||||
private readonly int _defaultMineBuildSeconds;
|
||||
|
||||
public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||
{
|
||||
@@ -29,9 +31,15 @@ public class LocationService
|
||||
_logCampWoodPerMinute = int.TryParse(cfg["Buildings:LogCampWoodPerMinute"], out var logCampWoodPerMinute)
|
||||
? Math.Max(1, logCampWoodPerMinute)
|
||||
: 5;
|
||||
_mineOrePerMinute = int.TryParse(cfg["Buildings:MineOrePerMinute"], out var mineOrePerMinute)
|
||||
? Math.Max(1, mineOrePerMinute)
|
||||
: 5;
|
||||
_defaultLogCampBuildSeconds = int.TryParse(cfg["Buildings:LogCampBuildSeconds"], out var logCampBuildSeconds)
|
||||
? Math.Max(1, logCampBuildSeconds)
|
||||
: 60;
|
||||
_defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds)
|
||||
? Math.Max(1, mineBuildSeconds)
|
||||
: 60;
|
||||
|
||||
var coordinateIndex = Builders<Location>.IndexKeys
|
||||
.Ascending(x => x.Q)
|
||||
@@ -123,7 +131,8 @@ public class LocationService
|
||||
|
||||
public async Task<Location?> BuildAsync(BuildLocationRequest request, string username)
|
||||
{
|
||||
if (!string.Equals(request.BuildingType, "LogCamp", StringComparison.OrdinalIgnoreCase))
|
||||
var buildingType = NormalizeBuildingType(request.BuildingType);
|
||||
if (buildingType is null)
|
||||
{
|
||||
throw new ArgumentException("Unsupported building type.", nameof(request.BuildingType));
|
||||
}
|
||||
@@ -140,17 +149,24 @@ public class LocationService
|
||||
throw new InvalidOperationException("This location already has a building or construction in progress.");
|
||||
}
|
||||
|
||||
if (!string.Equals(location.Terrain, "Forest", StringComparison.OrdinalIgnoreCase))
|
||||
var requiredTerrain = buildingType == "LogCamp" ? "Forest" : "Ore";
|
||||
if (!string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Log Camps can only be built on Forest territory.");
|
||||
throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory.");
|
||||
}
|
||||
|
||||
await CompleteFinishedBuildingsAsync(username);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var buildSeconds = await _settings.GetIntAsync(GameSettingsService.LogCampBuildSeconds, _defaultLogCampBuildSeconds);
|
||||
var settingKey = buildingType == "LogCamp"
|
||||
? GameSettingsService.LogCampBuildSeconds
|
||||
: GameSettingsService.MineBuildSeconds;
|
||||
var defaultBuildSeconds = buildingType == "LogCamp"
|
||||
? _defaultLogCampBuildSeconds
|
||||
: _defaultMineBuildSeconds;
|
||||
var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds);
|
||||
var update = Builders<Location>.Update
|
||||
.Set(x => x.BuildingType, "LogCamp")
|
||||
.Set(x => x.BuildingType, buildingType)
|
||||
.Set(x => x.BuildingStatus, "Building")
|
||||
.Set(x => x.BuildingStartedAtUtc, now)
|
||||
.Set(x => x.BuildingCompletesAtUtc, now.AddSeconds(buildSeconds))
|
||||
@@ -177,7 +193,7 @@ public class LocationService
|
||||
var now = DateTime.UtcNow;
|
||||
var readyBuildings = await _locations.Find(x =>
|
||||
x.OwnerUsername == username &&
|
||||
x.BuildingType == "LogCamp" &&
|
||||
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine") &&
|
||||
x.BuildingStatus == "Building" &&
|
||||
x.BuildingCompletesAtUtc <= now)
|
||||
.ToListAsync();
|
||||
@@ -199,7 +215,14 @@ public class LocationService
|
||||
|
||||
if (updated is not null)
|
||||
{
|
||||
await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute);
|
||||
if (updated.BuildingType == "LogCamp")
|
||||
{
|
||||
await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute);
|
||||
}
|
||||
else if (updated.BuildingType == "Mine")
|
||||
{
|
||||
await _resources.AddOreGenerationAsync(username, _mineOrePerMinute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,4 +574,15 @@ public class LocationService
|
||||
_ => "Field"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? NormalizeBuildingType(string buildingType)
|
||||
{
|
||||
var trimmed = buildingType.Trim();
|
||||
return trimmed.ToLowerInvariant() switch
|
||||
{
|
||||
"logcamp" or "log camp" => "LogCamp",
|
||||
"mine" => "Mine",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ public class ResourceService
|
||||
.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);
|
||||
|
||||
@@ -102,6 +104,28 @@ public class ResourceService
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -111,8 +135,11 @@ public class ResourceService
|
||||
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)
|
||||
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
@@ -135,6 +162,13 @@ public class ResourceService
|
||||
.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,
|
||||
|
||||
@@ -34,6 +34,7 @@ public class UnitTrainingService
|
||||
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed");
|
||||
|
||||
var all = await _training.Find(x => x.Username == username).ToListAsync();
|
||||
var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
|
||||
@@ -85,18 +86,11 @@ public class UnitTrainingService
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var update = Builders<UnitTraining>.Update
|
||||
.Set(x => x.Status, "Consumed")
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
var consumed = await _training.FindOneAndUpdateAsync(
|
||||
var consumed = await _training.FindOneAndDeleteAsync(
|
||||
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
||||
update,
|
||||
new FindOneAndUpdateOptions<UnitTraining>
|
||||
new FindOneAndDeleteOptions<UnitTraining>
|
||||
{
|
||||
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc),
|
||||
ReturnDocument = ReturnDocument.After
|
||||
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc)
|
||||
});
|
||||
|
||||
return consumed is not null;
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
},
|
||||
"Buildings": {
|
||||
"LogCampWoodPerMinute": 5,
|
||||
"LogCampBuildSeconds": 60
|
||||
"LogCampBuildSeconds": 60,
|
||||
"MineOrePerMinute": 5,
|
||||
"MineBuildSeconds": 60
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -17,3 +17,5 @@ data:
|
||||
Units__ScoutTrainingSeconds: "60"
|
||||
Buildings__LogCampWoodPerMinute: "5"
|
||||
Buildings__LogCampBuildSeconds: "60"
|
||||
Buildings__MineOrePerMinute: "5"
|
||||
Buildings__MineBuildSeconds: "60"
|
||||
|
||||
Reference in New Issue
Block a user