diff --git a/game/territory-autobattler/scenes/game/hex_grid_screen.gd b/game/territory-autobattler/scenes/game/hex_grid_screen.gd index e99bd97..9b60290 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_screen.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_screen.gd @@ -28,7 +28,7 @@ var escape_panel: PanelContainer var gold_label: Label var tick_bar: ProgressBar var tick_label: Label -var resource_text := "Gold: --\nWood: --\nOre: --" +var resource_text := "Gold: --\nWood: --\nOre: --\nFood: --" var gold_tick_anchor_unix := 0.0 var gold_tick_seconds := 60.0 var train_button: Button @@ -53,6 +53,7 @@ var active_training_label := "Training" var claim_button: Button var build_log_camp_button: Button var build_mine_button: Button +var build_farm_button: Button var build_barracks_button: Button var unit_action_popup: PopupPanel var unit_action_move_button: Button @@ -61,6 +62,7 @@ var selected_r := 0 var selected_can_claim := false var selected_can_build_log_camp := false var selected_can_build_mine := false +var selected_can_build_farm := false var selected_can_build_barracks := false var selected_can_train_archer := false var selected_is_home := false @@ -236,6 +238,11 @@ func _build_ui() -> void: build_mine_button.pressed.connect(_build_mine) action_buttons.add_child(build_mine_button) + build_farm_button = _small_button("Build Farm") + build_farm_button.visible = false + build_farm_button.pressed.connect(_build_farm) + action_buttons.add_child(build_farm_button) + build_barracks_button = _small_button("Build Barracks") build_barracks_button.visible = false build_barracks_button.pressed.connect(_build_barracks) @@ -455,6 +462,7 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void: selected_is_home = _selected_tile_is_home() selected_can_build_log_camp = _can_build_log_camp_on_selected_tile() selected_can_build_mine = _can_build_mine_on_selected_tile() + selected_can_build_farm = _can_build_farm_on_selected_tile() selected_can_build_barracks = _can_build_barracks_on_selected_tile() selected_can_train_archer = _selected_tile_has_built_barracks() _update_action_buttons() @@ -532,6 +540,10 @@ func _can_build_mine_on_selected_tile() -> bool: return _can_build_on_selected_tile("Ore") +func _can_build_farm_on_selected_tile() -> bool: + return _can_build_on_selected_tile("Field") + + func _can_build_barracks_on_selected_tile() -> bool: if not _can_build_on_selected_tile(""): return false @@ -577,6 +589,9 @@ func _update_action_buttons() -> void: if build_mine_button: build_mine_button.visible = selected_can_build_mine build_mine_button.disabled = false + if build_farm_button: + build_farm_button.visible = selected_can_build_farm + build_farm_button.disabled = false if build_barracks_button: build_barracks_button.visible = selected_can_build_barracks build_barracks_button.disabled = false @@ -832,7 +847,7 @@ func _refresh_player_state() -> void: func _load_resources() -> void: var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): - resource_text = "Gold: --\nWood: --\nOre: --" + resource_text = "Gold: --\nWood: --\nOre: --\nFood: --" gold_label.text = resource_text return @@ -873,7 +888,10 @@ func _on_resources_request_completed(result: int, response_code: int, _headers: var ore: int = int(resources.get("ore", 0)) var ore_per_minute: int = int(resources.get("orePerMinute", 0)) var ore_capacity: int = int(resources.get("oreCapacity", 100)) - resource_text = "Gold: %d/%d (+%d/min)\nWood: %d/%d (+%d/min)\nOre: %d/%d (+%d/min)" % [ + var food: int = int(resources.get("food", 0)) + var food_per_minute: int = int(resources.get("foodPerMinute", 0)) + var food_capacity: int = int(resources.get("foodCapacity", 100)) + resource_text = "Gold: %d/%d (+%d/min)\nWood: %d/%d (+%d/min)\nOre: %d/%d (+%d/min)\nFood: %d/%d (+%d/min)" % [ gold, gold_capacity, gold_per_minute, @@ -882,7 +900,10 @@ func _on_resources_request_completed(result: int, response_code: int, _headers: wood_per_minute, ore, ore_capacity, - ore_per_minute + ore_per_minute, + food, + food_capacity, + food_per_minute ] gold_label.text = resource_text @@ -1156,6 +1177,14 @@ func _build_mine() -> void: _request_building("Mine") +func _build_farm() -> void: + if not selected_can_build_farm: + coordinate_label.text = "Select controlled Field territory without a building." + return + + _request_building("Farm") + + func _build_barracks() -> void: if not selected_can_build_barracks: coordinate_label.text = "Select controlled territory without a building." @@ -1176,6 +1205,8 @@ func _request_building(building_type: String) -> void: match building_type: "Mine": build_mine_button.disabled = true + "Farm": + build_farm_button.disabled = true "Barracks": build_barracks_button.disabled = true _: @@ -1348,6 +1379,8 @@ func _building_display_name(building_type: String) -> String: return "Log Camp" "Mine": return "Mine" + "Farm": + return "Farm" "Barracks": return "Barracks" _: diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index bf587bf..504a903 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -184,6 +184,8 @@ func _draw_tile(tile: Dictionary) -> void: _draw_log_camp_marker(center, height) elif building_type == "Mine" and (building_status == "Built" or building_status.is_empty()): _draw_mine_marker(center, height) + elif building_type == "Farm" and (building_status == "Built" or building_status.is_empty()): + _draw_farm_marker(center, height) elif building_type == "Barracks" and (building_status == "Built" or building_status.is_empty()): _draw_barracks_marker(center, height) elif not building_type.is_empty(): @@ -356,6 +358,33 @@ func _draw_mine_marker(center: Vector2, height: float) -> void: draw_circle(marker_center + Vector2(width * 0.34, -marker_height * 0.1), 2.0 * zoom, ore_color) +func _draw_farm_marker(center: Vector2, height: float) -> void: + var marker_center: Vector2 = center + Vector2(0, -height * zoom - 2.0) + var width: float = 22.0 * zoom + var crop_color := Color(0.76, 0.67, 0.28) + var soil_color := Color(0.34, 0.22, 0.12) + var fence_color := Color(0.70, 0.52, 0.30) + var shadow_color := Color(0.08, 0.06, 0.04) + + var plot := PackedVector2Array([ + marker_center + Vector2(-width * 0.55, width * 0.18), + marker_center + Vector2(-width * 0.18, -width * 0.24), + marker_center + Vector2(width * 0.55, -width * 0.03), + marker_center + Vector2(width * 0.18, width * 0.34) + ]) + draw_colored_polygon(plot, soil_color) + draw_polyline(_closed_points(plot), fence_color, 1.5 * zoom) + for offset in [-0.28, 0.0, 0.28]: + draw_line( + marker_center + Vector2((-0.36 + offset) * width, width * 0.13), + marker_center + Vector2((-0.08 + offset) * width, -width * 0.16), + crop_color, + 1.8 * zoom + ) + draw_circle(marker_center + Vector2(width * 0.26, -width * 0.16), 2.2 * zoom, crop_color.lightened(0.18)) + draw_line(marker_center + Vector2(-width * 0.52, width * 0.25), marker_center + Vector2(width * 0.5, width * 0.0), shadow_color, 1.0 * zoom) + + func _draw_barracks_marker(center: Vector2, height: float) -> void: var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0) var width: float = 22.0 * zoom @@ -667,6 +696,8 @@ func _tile_display_label(key: String, data: Dictionary) -> String: building_label = " | Log Camp" if building_status == "Built" or building_status.is_empty() else " | Log Camp building" elif building_type == "Mine": building_label = " | Mine" if building_status == "Built" or building_status.is_empty() else " | Mine building" + elif building_type == "Farm": + building_label = " | Farm" if building_status == "Built" or building_status.is_empty() else " | Farm building" elif building_type == "Barracks": building_label = " | Barracks" if building_status == "Built" or building_status.is_empty() else " | Barracks building" var archer_count := _archer_count_at(int(data.get("q", 0)), int(data.get("r", 0))) diff --git a/microservices/LocationsApi/Models/Dto.cs b/microservices/LocationsApi/Models/Dto.cs index 2bf2340..d69ae4c 100644 --- a/microservices/LocationsApi/Models/Dto.cs +++ b/microservices/LocationsApi/Models/Dto.cs @@ -52,7 +52,7 @@ public class LocationResponse Label = location.Label, Level = location.Level, BuildingType = revealDetails ? location.BuildingType : null, - BuildingStatus = revealDetails && (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(location.BuildingStatus) + BuildingStatus = revealDetails && (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks" || location.BuildingType == "Farm") && string.IsNullOrWhiteSpace(location.BuildingStatus) ? "Built" : revealDetails ? location.BuildingStatus : null, BuildingStartedAtUtc = revealDetails ? location.BuildingStartedAtUtc : null, @@ -74,7 +74,7 @@ public class LocationResponse Label = intel.Label, Level = intel.Level, BuildingType = intel.BuildingType, - BuildingStatus = (intel.BuildingType == "LogCamp" || intel.BuildingType == "Mine" || intel.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(intel.BuildingStatus) + BuildingStatus = (intel.BuildingType == "LogCamp" || intel.BuildingType == "Mine" || intel.BuildingType == "Barracks" || intel.BuildingType == "Farm") && string.IsNullOrWhiteSpace(intel.BuildingStatus) ? "Built" : intel.BuildingStatus, BuildingStartedAtUtc = intel.BuildingStartedAtUtc, diff --git a/microservices/LocationsApi/Models/PlayerResources.cs b/microservices/LocationsApi/Models/PlayerResources.cs index b4bfaa5..3170c79 100644 --- a/microservices/LocationsApi/Models/PlayerResources.cs +++ b/microservices/LocationsApi/Models/PlayerResources.cs @@ -19,9 +19,13 @@ public class PlayerResources public int Ore { get; set; } public int OrePerMinute { get; set; } public int OreCapacity { get; set; } = 100; + public int Food { get; set; } + public int FoodPerMinute { get; set; } + public int FoodCapacity { get; set; } = 100; public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow; public DateTime LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow; public DateTime LastOreGeneratedAtUtc { get; set; } = DateTime.UtcNow; + public DateTime LastFoodGeneratedAtUtc { get; set; } = DateTime.UtcNow; public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow; } diff --git a/microservices/LocationsApi/Models/ResourceDto.cs b/microservices/LocationsApi/Models/ResourceDto.cs index 6f959a1..5facf79 100644 --- a/microservices/LocationsApi/Models/ResourceDto.cs +++ b/microservices/LocationsApi/Models/ResourceDto.cs @@ -12,9 +12,13 @@ public class PlayerResourcesResponse public int Ore { get; set; } public int OrePerMinute { get; set; } public int OreCapacity { get; set; } + public int Food { get; set; } + public int FoodPerMinute { get; set; } + public int FoodCapacity { get; set; } public DateTime LastGoldGeneratedAtUtc { get; set; } public DateTime LastWoodGeneratedAtUtc { get; set; } public DateTime LastOreGeneratedAtUtc { get; set; } + public DateTime LastFoodGeneratedAtUtc { get; set; } public DateTime UpdatedAtUtc { get; set; } public static PlayerResourcesResponse FromResources(PlayerResources resources) => new() @@ -29,9 +33,13 @@ public class PlayerResourcesResponse Ore = resources.Ore, OrePerMinute = resources.OrePerMinute, OreCapacity = resources.OreCapacity, + Food = resources.Food, + FoodPerMinute = resources.FoodPerMinute, + FoodCapacity = resources.FoodCapacity, LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc, LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc, LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc, + LastFoodGeneratedAtUtc = resources.LastFoodGeneratedAtUtc, UpdatedAtUtc = resources.UpdatedAtUtc }; } diff --git a/microservices/LocationsApi/Services/GameSettingsService.cs b/microservices/LocationsApi/Services/GameSettingsService.cs index 57fad5d..ead5feb 100644 --- a/microservices/LocationsApi/Services/GameSettingsService.cs +++ b/microservices/LocationsApi/Services/GameSettingsService.cs @@ -7,9 +7,25 @@ public class GameSettingsService { public const string ScoutTrainingSeconds = "ScoutTrainingSeconds"; public const string ArcherTrainingSeconds = "ArcherTrainingSeconds"; + public const string ScoutGoldCost = "ScoutGoldCost"; + public const string ArcherGoldCost = "ArcherGoldCost"; + public const string ArcherWoodCost = "ArcherWoodCost"; public const string LogCampBuildSeconds = "LogCampBuildSeconds"; public const string MineBuildSeconds = "MineBuildSeconds"; public const string BarracksBuildSeconds = "BarracksBuildSeconds"; + public const string FarmBuildSeconds = "FarmBuildSeconds"; + public const string BarracksGoldCost = "BarracksGoldCost"; + public const string BarracksWoodCost = "BarracksWoodCost"; + public const string BarracksOreCost = "BarracksOreCost"; + public const string LogCampWoodPerMinute = "LogCampWoodPerMinute"; + public const string MineOrePerMinute = "MineOrePerMinute"; + public const string FarmFoodPerMinute = "FarmFoodPerMinute"; + public const string StartingGold = "StartingGold"; + public const string HeadquartersGoldPerMinute = "HeadquartersGoldPerMinute"; + public const string TownHallGoldCapacity = "TownHallGoldCapacity"; + public const string TownHallWoodCapacity = "TownHallWoodCapacity"; + public const string TownHallOreCapacity = "TownHallOreCapacity"; + public const string TownHallFoodCapacity = "TownHallFoodCapacity"; private readonly IMongoCollection _settings; @@ -44,4 +60,23 @@ public class GameSettingsService return Math.Max(1, setting.IntValue); } + + public async Task GetNonNegativeIntAsync(string key, int defaultValue) + { + var now = DateTime.UtcNow; + var setting = await _settings.FindOneAndUpdateAsync( + x => x.Key == key, + Builders.Update + .SetOnInsert(x => x.Key, key) + .SetOnInsert(x => x.IntValue, defaultValue) + .SetOnInsert(x => x.CreatedAtUtc, now) + .Set(x => x.UpdatedAtUtc, now), + new FindOneAndUpdateOptions + { + IsUpsert = true, + ReturnDocument = ReturnDocument.After + }); + + return Math.Max(0, setting.IntValue); + } } diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index c5a7f4d..4055442 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -15,9 +15,11 @@ public class LocationService private readonly int _visibilityDepth; private readonly int _logCampWoodPerMinute; private readonly int _mineOrePerMinute; + private readonly int _farmFoodPerMinute; private readonly int _defaultLogCampBuildSeconds; private readonly int _defaultMineBuildSeconds; private readonly int _defaultBarracksBuildSeconds; + private readonly int _defaultFarmBuildSeconds; private readonly int _barracksGoldCost; private readonly int _barracksWoodCost; private readonly int _barracksOreCost; @@ -42,6 +44,9 @@ public class LocationService _mineOrePerMinute = int.TryParse(cfg["Buildings:MineOrePerMinute"], out var mineOrePerMinute) ? Math.Max(1, mineOrePerMinute) : 5; + _farmFoodPerMinute = int.TryParse(cfg["Buildings:FarmFoodPerMinute"], out var farmFoodPerMinute) + ? Math.Max(1, farmFoodPerMinute) + : 5; _defaultLogCampBuildSeconds = int.TryParse(cfg["Buildings:LogCampBuildSeconds"], out var logCampBuildSeconds) ? Math.Max(1, logCampBuildSeconds) : 60; @@ -51,6 +56,9 @@ public class LocationService _defaultBarracksBuildSeconds = int.TryParse(cfg["Buildings:BarracksBuildSeconds"], out var barracksBuildSeconds) ? Math.Max(1, barracksBuildSeconds) : 60; + _defaultFarmBuildSeconds = int.TryParse(cfg["Buildings:FarmBuildSeconds"], out var farmBuildSeconds) + ? Math.Max(1, farmBuildSeconds) + : 60; _barracksGoldCost = int.TryParse(cfg["Buildings:BarracksGoldCost"], out var barracksGoldCost) ? Math.Max(0, barracksGoldCost) : 500; @@ -215,7 +223,10 @@ public class LocationService if (buildingType == "Barracks") { - var spent = await _resources.TrySpendResourcesAsync(username, _barracksGoldCost, _barracksWoodCost, _barracksOreCost); + var barracksGoldCost = await _settings.GetNonNegativeIntAsync(GameSettingsService.BarracksGoldCost, _barracksGoldCost); + var barracksWoodCost = await _settings.GetNonNegativeIntAsync(GameSettingsService.BarracksWoodCost, _barracksWoodCost); + var barracksOreCost = await _settings.GetNonNegativeIntAsync(GameSettingsService.BarracksOreCost, _barracksOreCost); + var spent = await _resources.TrySpendResourcesAsync(username, barracksGoldCost, barracksWoodCost, barracksOreCost); if (!spent) { var rollback = Builders.Update @@ -230,7 +241,7 @@ public class LocationService 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."); + throw new InvalidOperationException($"Not enough resources to build Barracks. Required: {barracksGoldCost} gold, {barracksWoodCost} wood, {barracksOreCost} ore."); } } @@ -242,7 +253,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 == "Barracks") && + (x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Barracks" || x.BuildingType == "Farm") && x.BuildingStatus == "Building" && x.BuildingCompletesAtUtc <= now) .ToListAsync(); @@ -266,11 +277,18 @@ public class LocationService { if (updated.BuildingType == "LogCamp") { - await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute); + var woodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.LogCampWoodPerMinute, _logCampWoodPerMinute); + await _resources.AddWoodGenerationAsync(username, woodPerMinute); } else if (updated.BuildingType == "Mine") { - await _resources.AddOreGenerationAsync(username, _mineOrePerMinute); + var orePerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.MineOrePerMinute, _mineOrePerMinute); + await _resources.AddOreGenerationAsync(username, orePerMinute); + } + else if (updated.BuildingType == "Farm") + { + var foodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, _farmFoodPerMinute); + await _resources.AddFoodGenerationAsync(username, foodPerMinute); } } } @@ -758,6 +776,7 @@ public class LocationService "logcamp" or "log camp" => "LogCamp", "mine" => "Mine", "barracks" => "Barracks", + "farm" => "Farm", _ => null }; } @@ -767,6 +786,7 @@ public class LocationService { "LogCamp" => "Forest", "Mine" => "Ore", + "Farm" => "Field", "Barracks" => null, _ => null }; @@ -777,6 +797,7 @@ public class LocationService "LogCamp" => GameSettingsService.LogCampBuildSeconds, "Mine" => GameSettingsService.MineBuildSeconds, "Barracks" => GameSettingsService.BarracksBuildSeconds, + "Farm" => GameSettingsService.FarmBuildSeconds, _ => GameSettingsService.LogCampBuildSeconds }; @@ -786,6 +807,7 @@ public class LocationService "LogCamp" => _defaultLogCampBuildSeconds, "Mine" => _defaultMineBuildSeconds, "Barracks" => _defaultBarracksBuildSeconds, + "Farm" => _defaultFarmBuildSeconds, _ => 60 }; } diff --git a/microservices/LocationsApi/Services/ResourceService.cs b/microservices/LocationsApi/Services/ResourceService.cs index c1c7322..4d92c46 100644 --- a/microservices/LocationsApi/Services/ResourceService.cs +++ b/microservices/LocationsApi/Services/ResourceService.cs @@ -6,19 +6,22 @@ namespace LocationsApi.Services; public class ResourceService { private readonly IMongoCollection _resources; + private readonly GameSettingsService _settings; private readonly int _startingGold; private readonly int _headquartersGoldPerMinute; private readonly int _townHallGoldCapacity; private readonly int _townHallWoodCapacity; private readonly int _townHallOreCapacity; + private readonly int _townHallFoodCapacity; - public ResourceService(IConfiguration cfg) + public ResourceService(IConfiguration cfg, GameSettingsService settings) { 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"); + _settings = settings; _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) @@ -32,6 +35,9 @@ public class ResourceService _townHallOreCapacity = int.TryParse(cfg["Resources:TownHallOreCapacity"], out var oreCapacity) ? Math.Max(0, oreCapacity) : 100; + _townHallFoodCapacity = int.TryParse(cfg["Resources:TownHallFoodCapacity"], out var foodCapacity) + ? Math.Max(0, foodCapacity) + : 100; _resources.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(x => x.Username), @@ -41,19 +47,28 @@ public class ResourceService public async Task EnsureAsync(string username) { var now = DateTime.UtcNow; + var startingGold = await _settings.GetNonNegativeIntAsync(GameSettingsService.StartingGold, _startingGold); + var goldPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.HeadquartersGoldPerMinute, _headquartersGoldPerMinute); + var goldCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallGoldCapacity, _townHallGoldCapacity); + var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity); + var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity); + var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity); var update = Builders.Update .SetOnInsert(x => x.Username, username) - .SetOnInsert(x => x.Gold, Math.Min(_startingGold, _townHallGoldCapacity)) + .SetOnInsert(x => x.Gold, Math.Min(startingGold, goldCapacity)) .SetOnInsert(x => x.Wood, 0) .SetOnInsert(x => x.Ore, 0) + .SetOnInsert(x => x.Food, 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.GoldCapacity, _townHallGoldCapacity) - .Set(x => x.WoodCapacity, _townHallWoodCapacity) - .Set(x => x.OreCapacity, _townHallOreCapacity) + .SetOnInsert(x => x.LastFoodGeneratedAtUtc, now) + .Set(x => x.GoldPerMinute, goldPerMinute) + .Set(x => x.GoldCapacity, goldCapacity) + .Set(x => x.WoodCapacity, woodCapacity) + .Set(x => x.OreCapacity, oreCapacity) + .Set(x => x.FoodCapacity, foodCapacity) .Set(x => x.UpdatedAtUtc, now); var resources = await _resources.FindOneAndUpdateAsync( @@ -65,7 +80,7 @@ public class ResourceService ReturnDocument = ReturnDocument.After }); - return await ApplyGoldGenerationAsync(resources); + return await ApplyResourceGenerationAsync(resources); } public async Task GetAsync(string username) @@ -170,10 +185,37 @@ public class ResourceService await _resources.UpdateOneAsync(x => x.Username == username, update); } - private async Task ApplyGoldGenerationAsync(PlayerResources resources) + public async Task AddFoodGenerationAsync(string username, int foodPerMinute) + { + if (foodPerMinute <= 0) + { + return; + } + + var resources = await EnsureAsync(username); + var now = DateTime.UtcNow; + + var update = Builders.Update + .Inc(x => x.FoodPerMinute, foodPerMinute) + .Set(x => x.UpdatedAtUtc, now); + + if (resources.FoodPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000) + { + update = update.Set(x => x.LastFoodGeneratedAtUtc, now); + } + + await _resources.UpdateOneAsync(x => x.Username == username, update); + } + + private async Task ApplyResourceGenerationAsync(PlayerResources resources) { var now = DateTime.UtcNow; - var goldElapsedMinutes = resources.GoldPerMinute <= 0 + var goldPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.HeadquartersGoldPerMinute, _headquartersGoldPerMinute); + var goldCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallGoldCapacity, _townHallGoldCapacity); + var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity); + var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity); + var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity); + var goldElapsedMinutes = goldPerMinute <= 0 ? 0 : (int)Math.Floor((now - resources.LastGoldGeneratedAtUtc).TotalMinutes); var woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000 @@ -182,21 +224,26 @@ 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; + var foodElapsedMinutes = resources.FoodPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000 + ? 0 + : (int)Math.Floor((now - resources.LastFoodGeneratedAtUtc).TotalMinutes); + var cappedGold = Math.Min(resources.Gold, goldCapacity); + var cappedWood = Math.Min(resources.Wood, woodCapacity); + var cappedOre = Math.Min(resources.Ore, oreCapacity); + var cappedFood = Math.Min(resources.Food, foodCapacity); + var isOverCapacity = resources.Gold != cappedGold || resources.Wood != cappedWood || resources.Ore != cappedOre || resources.Food != cappedFood; - if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0 && !isOverCapacity) + if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0 && foodElapsedMinutes <= 0 && !isOverCapacity) { return resources; } var update = Builders.Update - .Set(x => x.GoldPerMinute, _headquartersGoldPerMinute) - .Set(x => x.GoldCapacity, _townHallGoldCapacity) - .Set(x => x.WoodCapacity, _townHallWoodCapacity) - .Set(x => x.OreCapacity, _townHallOreCapacity) + .Set(x => x.GoldPerMinute, goldPerMinute) + .Set(x => x.GoldCapacity, goldCapacity) + .Set(x => x.WoodCapacity, woodCapacity) + .Set(x => x.OreCapacity, oreCapacity) + .Set(x => x.FoodCapacity, foodCapacity) .Set(x => x.UpdatedAtUtc, now); if (isOverCapacity) @@ -204,12 +251,13 @@ public class ResourceService update = update .Set(x => x.Gold, cappedGold) .Set(x => x.Wood, cappedWood) - .Set(x => x.Ore, cappedOre); + .Set(x => x.Ore, cappedOre) + .Set(x => x.Food, cappedFood); } if (goldElapsedMinutes > 0) { - var generatedGold = Math.Min(_townHallGoldCapacity, cappedGold + (goldElapsedMinutes * resources.GoldPerMinute)); + var generatedGold = Math.Min(goldCapacity, cappedGold + (goldElapsedMinutes * goldPerMinute)); update = update .Set(x => x.Gold, generatedGold) .Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes)); @@ -217,7 +265,7 @@ public class ResourceService if (woodElapsedMinutes > 0) { - var generatedWood = Math.Min(_townHallWoodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute)); + var generatedWood = Math.Min(woodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute)); update = update .Set(x => x.Wood, generatedWood) .Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes)); @@ -225,12 +273,20 @@ public class ResourceService if (oreElapsedMinutes > 0) { - var generatedOre = Math.Min(_townHallOreCapacity, cappedOre + (oreElapsedMinutes * resources.OrePerMinute)); + var generatedOre = Math.Min(oreCapacity, cappedOre + (oreElapsedMinutes * resources.OrePerMinute)); update = update .Set(x => x.Ore, generatedOre) .Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes)); } + if (foodElapsedMinutes > 0) + { + var generatedFood = Math.Min(foodCapacity, cappedFood + (foodElapsedMinutes * resources.FoodPerMinute)); + update = update + .Set(x => x.Food, generatedFood) + .Set(x => x.LastFoodGeneratedAtUtc, resources.LastFoodGeneratedAtUtc.AddMinutes(foodElapsedMinutes)); + } + return await _resources.FindOneAndUpdateAsync( x => x.Username == resources.Username, update, diff --git a/microservices/LocationsApi/Services/UnitTrainingService.cs b/microservices/LocationsApi/Services/UnitTrainingService.cs index c6d7ed1..d0ed4d4 100644 --- a/microservices/LocationsApi/Services/UnitTrainingService.cs +++ b/microservices/LocationsApi/Services/UnitTrainingService.cs @@ -107,8 +107,12 @@ public class UnitTrainingService } } - var goldCost = normalizedUnitType == ArcherUnitType ? _archerGoldCost : _scoutGoldCost; - var woodCost = normalizedUnitType == ArcherUnitType ? _archerWoodCost : 0; + var goldCost = normalizedUnitType == ArcherUnitType + ? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherGoldCost, _archerGoldCost) + : await _settings.GetNonNegativeIntAsync(GameSettingsService.ScoutGoldCost, _scoutGoldCost); + var woodCost = normalizedUnitType == ArcherUnitType + ? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherWoodCost, _archerWoodCost) + : 0; var spent = normalizedUnitType == ArcherUnitType ? await _resources.TrySpendResourcesAsync(username, goldCost, woodCost, 0) : await _resources.TrySpendGoldAsync(username, goldCost); @@ -241,7 +245,9 @@ public class UnitTrainingService { var hasOtherUnits = await _units.Find(x => x.Username != username && x.Q == q && x.R == r).AnyAsync(); var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync(); - var hasBuilding = target is not null && !string.IsNullOrWhiteSpace(target.BuildingType); + var hasBuilding = target is not null && + (!string.IsNullOrWhiteSpace(target.BuildingType) || + string.Equals(target.Kind, "Home", StringComparison.OrdinalIgnoreCase)); if (hasOtherUnits || hasBuilding) { @@ -278,14 +284,15 @@ public class UnitTrainingService return; } - var emptyBuildingFilter = Builders.Filter.And( + var capturableFilter = Builders.Filter.And( Builders.Filter.Eq(x => x.Id, target.Id), + Builders.Filter.Ne(x => x.Kind, "Home"), Builders.Filter.Or( Builders.Filter.Eq(x => x.BuildingType, null), Builders.Filter.Eq(x => x.BuildingType, ""))); await _locations.UpdateOneAsync( - emptyBuildingFilter, + capturableFilter, Builders.Update .Set(x => x.OwnerUsername, username) .Set(x => x.Kind, "Claimed") diff --git a/microservices/LocationsApi/appsettings.json b/microservices/LocationsApi/appsettings.json index f8f3d7e..47d2b23 100644 --- a/microservices/LocationsApi/appsettings.json +++ b/microservices/LocationsApi/appsettings.json @@ -24,7 +24,8 @@ "HeadquartersGoldPerMinute": 10, "TownHallGoldCapacity": 500, "TownHallWoodCapacity": 100, - "TownHallOreCapacity": 100 + "TownHallOreCapacity": 100, + "TownHallFoodCapacity": 100 }, "Units": { "ScoutGoldCost": 50, @@ -38,7 +39,9 @@ "LogCampBuildSeconds": 60, "MineOrePerMinute": 5, "MineBuildSeconds": 60, + "FarmFoodPerMinute": 5, "BarracksBuildSeconds": 60, + "FarmBuildSeconds": 60, "BarracksGoldCost": 500, "BarracksWoodCost": 100, "BarracksOreCost": 100 diff --git a/microservices/LocationsApi/k8s/configmap.yaml b/microservices/LocationsApi/k8s/configmap.yaml index 9468547..0e3bf62 100644 --- a/microservices/LocationsApi/k8s/configmap.yaml +++ b/microservices/LocationsApi/k8s/configmap.yaml @@ -16,6 +16,7 @@ data: Resources__TownHallGoldCapacity: "500" Resources__TownHallWoodCapacity: "100" Resources__TownHallOreCapacity: "100" + Resources__TownHallFoodCapacity: "100" Units__ScoutGoldCost: "50" Units__ScoutTrainingSeconds: "60" Units__ArcherGoldCost: "100" @@ -25,7 +26,9 @@ data: Buildings__LogCampBuildSeconds: "60" Buildings__MineOrePerMinute: "5" Buildings__MineBuildSeconds: "60" + Buildings__FarmFoodPerMinute: "5" Buildings__BarracksBuildSeconds: "60" + Buildings__FarmBuildSeconds: "60" Buildings__BarracksGoldCost: "500" Buildings__BarracksWoodCost: "100" Buildings__BarracksOreCost: "100"