adding mines

This commit is contained in:
hz
2026-05-18 21:37:48 -05:00
parent 973eda5b44
commit 34d406e993
12 changed files with 294 additions and 45 deletions
+1 -1
View File
@@ -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
};
}
+10 -8
View File
@@ -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;
+3 -1
View File
@@ -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"