diff --git a/game/territory-autobattler/scenes/game/hex_grid_screen.gd b/game/territory-autobattler/scenes/game/hex_grid_screen.gd index 27eff06..bf685b5 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_screen.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_screen.gd @@ -36,12 +36,17 @@ var training_completion_refresh_requested := false var building_started_unix := 0.0 var building_completes_unix := 0.0 var building_completion_refresh_requested := false +var active_building_label := "Build" var claim_button: Button var build_log_camp_button: Button +var build_mine_button: Button var selected_q := 0 var selected_r := 0 var selected_can_claim := false var selected_can_build_log_camp := false +var selected_can_build_mine := false +var selected_is_home := false +var completed_scout_count := 0 func _ready() -> void: @@ -239,6 +244,7 @@ func _build_ui() -> void: building_box.add_child(building_bar) train_button = _small_button("Train Scout 50g") + train_button.visible = false train_button.pressed.connect(_train_scout) bottom_content.add_child(train_button) @@ -252,6 +258,11 @@ func _build_ui() -> void: build_log_camp_button.pressed.connect(_build_log_camp) bottom_content.add_child(build_log_camp_button) + build_mine_button = _small_button("Build Mine") + build_mine_button.disabled = true + build_mine_button.pressed.connect(_build_mine) + bottom_content.add_child(build_mine_button) + http_request = HTTPRequest.new() http_request.request_completed.connect(_on_home_request_completed) add_child(http_request) @@ -298,15 +309,63 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void: selected_q = q selected_r = r selected_can_claim = terrain.contains("Unclaimed") - selected_can_build_log_camp = terrain.contains("Forest") and terrain.contains("Your territory") and not terrain.contains("Log Camp") + 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() + if train_button: + train_button.visible = selected_is_home if claim_button: - claim_button.disabled = not selected_can_claim + claim_button.disabled = not _can_claim_selected_tile() if build_log_camp_button: build_log_camp_button.disabled = not selected_can_build_log_camp + if build_mine_button: + build_mine_button.disabled = not selected_can_build_mine if coordinate_label: coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain] +func _can_build_log_camp_on_selected_tile() -> bool: + return _can_build_on_selected_tile("Forest") + + +func _can_build_mine_on_selected_tile() -> bool: + return _can_build_on_selected_tile("Ore") + + +func _can_build_on_selected_tile(required_terrain: String) -> bool: + if not grid_view: + return false + + var location: Dictionary = grid_view.get_visible_location(selected_q, selected_r) + if location.is_empty(): + return false + + var username: String = str(session.get("username", "")) + var owner_username: String = str(location.get("ownerUsername", "")) + var terrain: String = str(location.get("terrain", "")) + var building_type: String = str(location.get("buildingType", "")) + + return owner_username == username and terrain == required_terrain and building_type.is_empty() + + +func _can_claim_selected_tile() -> bool: + return selected_can_claim and completed_scout_count > 0 + + +func _selected_tile_is_home() -> bool: + if not grid_view: + return false + + var location: Dictionary = grid_view.get_visible_location(selected_q, selected_r) + if location.is_empty(): + return false + + var username: String = str(session.get("username", "")) + var owner_username: String = str(location.get("ownerUsername", "")) + var kind: String = str(location.get("kind", "")) + return owner_username == username and kind == "Home" + + func _session_name() -> String: if not session.is_empty(): var session_username: String = str(session.get("username", "Signed in")) @@ -457,7 +516,9 @@ func _on_resources_request_completed(result: int, response_code: int, _headers: var gold_per_minute: int = int(resources.get("goldPerMinute", 0)) var wood: int = int(resources.get("wood", 0)) var wood_per_minute: int = int(resources.get("woodPerMinute", 0)) - gold_label.text = "Gold: %d (+%d/min) | Wood: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute] + var ore: int = int(resources.get("ore", 0)) + var ore_per_minute: int = int(resources.get("orePerMinute", 0)) + gold_label.text = "Gold: %d (+%d/min) | Wood: %d (+%d/min) | Ore: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute, ore, ore_per_minute] var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", "")) gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text) @@ -502,7 +563,10 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack if typeof(raw_training) == TYPE_ARRAY: training = raw_training - unit_label.text = "Scouts: %d | Queue: %d" % [int(units.get("completedScouts", 0)), training.size()] + completed_scout_count = int(units.get("completedScouts", 0)) + unit_label.text = "Scouts: %d | Queue: %d" % [completed_scout_count, training.size()] + if claim_button: + claim_button.disabled = not _can_claim_selected_tile() if training.is_empty(): training_started_unix = 0.0 @@ -573,6 +637,9 @@ func _claim_selected_tile() -> void: if not selected_can_claim: coordinate_label.text = "Select a visible unclaimed tile to send a scout." return + if completed_scout_count <= 0: + coordinate_label.text = "Train a scout before claiming territory." + return var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): @@ -623,16 +690,32 @@ func _build_log_camp() -> void: coordinate_label.text = "Select controlled Forest territory without a building." return + _request_building("LogCamp") + + +func _build_mine() -> void: + if not selected_can_build_mine: + coordinate_label.text = "Select controlled Ore territory without a building." + return + + _request_building("Mine") + + +func _request_building(building_type: String) -> void: var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): - coordinate_label.text = "No auth token available. Log Camp was not built." + coordinate_label.text = "No auth token available. %s was not built." % building_type return if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return - build_log_camp_button.disabled = true - coordinate_label.text = "Building Log Camp at q:%d r:%d..." % [selected_q, selected_r] + if building_type == "Mine": + build_mine_button.disabled = true + else: + build_log_camp_button.disabled = true + + coordinate_label.text = "Building %s at q:%d r:%d..." % [building_type, selected_q, selected_r] var error: int = build_request.request( LOCATIONS_BASE_URL + "/api/locations/build", PackedStringArray([ @@ -640,36 +723,42 @@ func _build_log_camp() -> void: "Authorization: Bearer %s" % access_token ]), HTTPClient.METHOD_POST, - JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": "LogCamp" }) + JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": building_type }) ) if error != OK: - build_log_camp_button.disabled = false + build_log_camp_button.disabled = not selected_can_build_log_camp + build_mine_button.disabled = not selected_can_build_mine coordinate_label.text = "Could not contact the locations service." func _on_build_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var text: String = body.get_string_from_utf8() if result != HTTPRequest.RESULT_SUCCESS: - coordinate_label.text = "Log Camp build request failed." + coordinate_label.text = "Build request failed." build_log_camp_button.disabled = not selected_can_build_log_camp + build_mine_button.disabled = not selected_can_build_mine return if response_code < 200 or response_code >= 300: - coordinate_label.text = "Log Camp build failed: %s" % text + coordinate_label.text = "Build failed: %s" % text build_log_camp_button.disabled = not selected_can_build_log_camp + build_mine_button.disabled = not selected_can_build_mine _refresh_player_state() return selected_can_build_log_camp = false + selected_can_build_mine = false build_log_camp_button.disabled = true + build_mine_button.disabled = true var parsed = JSON.parse_string(text) if typeof(parsed) == TYPE_DICTIONARY: var location: Dictionary = parsed + active_building_label = _building_display_name(str(location.get("buildingType", "Building"))) building_started_unix = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", ""))) building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", ""))) building_completion_refresh_requested = false _update_building_timer() - coordinate_label.text = "Log Camp construction started. Completion takes 1 minute." + coordinate_label.text = "%s construction started." % active_building_label _load_visible_locations() _refresh_player_state() @@ -733,9 +822,9 @@ func _update_building_timer() -> void: building_bar.value = elapsed / duration if remaining > 0: - building_label.text = "Log Camp: %ds" % remaining + building_label.text = "%s: %ds" % [active_building_label, remaining] else: - building_label.text = "Log Camp: ready" + building_label.text = "%s: ready" % active_building_label if remaining <= 0 and not building_completion_refresh_requested: building_completion_refresh_requested = true @@ -746,6 +835,7 @@ func _update_building_timer() -> void: func _sync_active_building_timer(locations: Array, username: String) -> void: var best_started := 0.0 var best_completes := 0.0 + var best_label := "Build" for item in locations: if typeof(item) != TYPE_DICTIONARY: @@ -754,7 +844,8 @@ func _sync_active_building_timer(locations: Array, username: String) -> void: var location: Dictionary = item if str(location.get("ownerUsername", "")) != username: continue - if str(location.get("buildingType", "")) != "LogCamp": + var building_type: String = str(location.get("buildingType", "")) + if building_type.is_empty(): continue if str(location.get("buildingStatus", "")) != "Building": continue @@ -766,13 +857,25 @@ func _sync_active_building_timer(locations: Array, username: String) -> void: if best_completes <= 0.0 or completes < best_completes: best_started = started best_completes = completes + best_label = _building_display_name(building_type) building_started_unix = best_started building_completes_unix = best_completes + active_building_label = best_label building_completion_refresh_requested = false _update_building_timer() +func _building_display_name(building_type: String) -> String: + match building_type: + "LogCamp": + return "Log Camp" + "Mine": + return "Mine" + _: + return "Build" + + func _parse_utc_timestamp(timestamp: String) -> float: if timestamp.is_empty(): return 0.0 diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index e38cf23..8e5815f 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -163,9 +163,13 @@ func _draw_tile(tile: Dictionary) -> void: var visible_location: Dictionary = visible_tiles.get(key, {}) var building_type: String = str(visible_location.get("buildingType", "")) var building_status: String = str(visible_location.get("buildingStatus", "")) - if building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()): + if coord == home_tile: + _draw_town_hall_marker(center, height) + elif building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()): _draw_log_camp_marker(center, height) - elif building_type == "LogCamp": + elif building_type == "Mine" and (building_status == "Built" or building_status.is_empty()): + _draw_mine_marker(center, height) + elif not building_type.is_empty(): _draw_building_marker(center, height) @@ -301,6 +305,61 @@ func _draw_log_camp_marker(center: Vector2, height: float) -> void: draw_line(marker_center + Vector2(-width * 0.38, marker_height * 0.86), marker_center + Vector2(width * 0.38, marker_height * 0.86), log_color.darkened(0.12), 3.0 * zoom) +func _draw_mine_marker(center: Vector2, height: float) -> void: + var marker_center: Vector2 = center + Vector2(0, -height * zoom - 2.0) + var width: float = 20.0 * zoom + var marker_height: float = 13.0 * zoom + var rock_color := Color(0.28, 0.31, 0.34) + var entry_color := Color(0.08, 0.09, 0.1) + var beam_color := Color(0.62, 0.46, 0.26) + var ore_color := Color(0.72, 0.82, 0.9) + + var mound := PackedVector2Array([ + marker_center + Vector2(-width * 0.56, marker_height * 0.48), + marker_center + Vector2(-width * 0.32, -marker_height * 0.22), + marker_center + Vector2(0, -marker_height * 0.52), + marker_center + Vector2(width * 0.38, -marker_height * 0.18), + marker_center + Vector2(width * 0.58, marker_height * 0.48) + ]) + draw_colored_polygon(mound, rock_color) + draw_rect(Rect2(marker_center + Vector2(-width * 0.18, marker_height * 0.02), Vector2(width * 0.36, marker_height * 0.48)), entry_color, true) + draw_line(marker_center + Vector2(-width * 0.28, marker_height * 0.02), marker_center + Vector2(width * 0.28, marker_height * 0.02), beam_color, 2.0 * zoom) + draw_line(marker_center + Vector2(-width * 0.18, marker_height * 0.02), marker_center + Vector2(-width * 0.18, marker_height * 0.5), beam_color, 2.0 * zoom) + draw_line(marker_center + Vector2(width * 0.18, marker_height * 0.02), marker_center + Vector2(width * 0.18, marker_height * 0.5), beam_color, 2.0 * zoom) + draw_circle(marker_center + Vector2(width * 0.34, -marker_height * 0.1), 2.0 * zoom, ore_color) + + +func _draw_town_hall_marker(center: Vector2, height: float) -> void: + var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0) + var width: float = 24.0 * zoom + var marker_height: float = 18.0 * zoom + var wall_color := Color(0.76, 0.58, 0.32) + var roof_color := Color(0.42, 0.19, 0.12) + var trim_color := Color(0.98, 0.78, 0.3) + var shadow_color := Color(0.12, 0.08, 0.04) + + var body_rect := Rect2(marker_center + Vector2(-width * 0.38, -marker_height * 0.02), Vector2(width * 0.76, marker_height * 0.62)) + var roof := PackedVector2Array([ + marker_center + Vector2(-width * 0.54, -marker_height * 0.02), + marker_center + Vector2(0, -marker_height * 0.72), + marker_center + Vector2(width * 0.54, -marker_height * 0.02) + ]) + var tower_rect := Rect2(marker_center + Vector2(-width * 0.13, -marker_height * 0.45), Vector2(width * 0.26, marker_height * 0.36)) + var tower_roof := PackedVector2Array([ + marker_center + Vector2(-width * 0.20, -marker_height * 0.45), + marker_center + Vector2(0, -marker_height * 0.84), + marker_center + Vector2(width * 0.20, -marker_height * 0.45) + ]) + + draw_rect(Rect2(body_rect.position + Vector2(1, 1) * zoom, body_rect.size), shadow_color, true) + draw_colored_polygon(roof, roof_color) + draw_rect(body_rect, wall_color, true) + draw_rect(tower_rect, wall_color.lightened(0.08), true) + draw_colored_polygon(tower_roof, roof_color.darkened(0.05)) + draw_line(body_rect.position + Vector2(0, body_rect.size.y), body_rect.position + body_rect.size, trim_color, 2.0 * zoom) + draw_rect(Rect2(marker_center + Vector2(-width * 0.08, marker_height * 0.28), Vector2(width * 0.16, marker_height * 0.32)), shadow_color.lightened(0.2), true) + + func _draw_building_marker(center: Vector2, height: float) -> void: var marker_center: Vector2 = center + Vector2(0, -height * zoom - 1.0) var width: float = 20.0 * zoom @@ -368,6 +427,13 @@ func set_visible_locations(locations: Array, username: String) -> void: queue_redraw() +func get_visible_location(q: int, r: int) -> Dictionary: + var key: String = _tile_key(q, r) + if visible_tiles.has(key): + return visible_tiles[key] + return {} + + func _tile_key(q: int, r: int) -> String: return "%d,%d" % [q, r] @@ -420,6 +486,8 @@ func _tile_display_label(key: String, data: Dictionary) -> String: var building_label: String = "" if building_type == "LogCamp": 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" if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))): return "%s | Your home%s" % [terrain, building_label] diff --git a/microservices/LocationsApi/Models/Dto.cs b/microservices/LocationsApi/Models/Dto.cs index 986d62c..de1d016 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 = 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, diff --git a/microservices/LocationsApi/Models/PlayerResources.cs b/microservices/LocationsApi/Models/PlayerResources.cs index c43404a..c30a707 100644 --- a/microservices/LocationsApi/Models/PlayerResources.cs +++ b/microservices/LocationsApi/Models/PlayerResources.cs @@ -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; } diff --git a/microservices/LocationsApi/Models/ResourceDto.cs b/microservices/LocationsApi/Models/ResourceDto.cs index 7b14030..d385cc0 100644 --- a/microservices/LocationsApi/Models/ResourceDto.cs +++ b/microservices/LocationsApi/Models/ResourceDto.cs @@ -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 }; } diff --git a/microservices/LocationsApi/README.md b/microservices/LocationsApi/README.md index 8a464c4..acbd0ce 100644 --- a/microservices/LocationsApi/README.md +++ b/microservices/LocationsApi/README.md @@ -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` diff --git a/microservices/LocationsApi/Services/GameSettingsService.cs b/microservices/LocationsApi/Services/GameSettingsService.cs index 2782d94..1cce4c1 100644 --- a/microservices/LocationsApi/Services/GameSettingsService.cs +++ b/microservices/LocationsApi/Services/GameSettingsService.cs @@ -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 _settings; diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index 545df59..8981174 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -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.IndexKeys .Ascending(x => x.Q) @@ -123,7 +131,8 @@ public class LocationService public async Task 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.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 + }; + } } diff --git a/microservices/LocationsApi/Services/ResourceService.cs b/microservices/LocationsApi/Services/ResourceService.cs index 0299920..0eafd6a 100644 --- a/microservices/LocationsApi/Services/ResourceService.cs +++ b/microservices/LocationsApi/Services/ResourceService.cs @@ -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.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 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, diff --git a/microservices/LocationsApi/Services/UnitTrainingService.cs b/microservices/LocationsApi/Services/UnitTrainingService.cs index 3bacba4..227366d 100644 --- a/microservices/LocationsApi/Services/UnitTrainingService.cs +++ b/microservices/LocationsApi/Services/UnitTrainingService.cs @@ -34,6 +34,7 @@ public class UnitTrainingService public async Task 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.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 + new FindOneAndDeleteOptions { - Sort = Builders.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc), - ReturnDocument = ReturnDocument.After + Sort = Builders.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc) }); return consumed is not null; diff --git a/microservices/LocationsApi/appsettings.json b/microservices/LocationsApi/appsettings.json index 3102cc0..559e4f4 100644 --- a/microservices/LocationsApi/appsettings.json +++ b/microservices/LocationsApi/appsettings.json @@ -29,7 +29,9 @@ }, "Buildings": { "LogCampWoodPerMinute": 5, - "LogCampBuildSeconds": 60 + "LogCampBuildSeconds": 60, + "MineOrePerMinute": 5, + "MineBuildSeconds": 60 }, "AllowedHosts": "*" } diff --git a/microservices/LocationsApi/k8s/configmap.yaml b/microservices/LocationsApi/k8s/configmap.yaml index c34d2b9..ffff5c6 100644 --- a/microservices/LocationsApi/k8s/configmap.yaml +++ b/microservices/LocationsApi/k8s/configmap.yaml @@ -17,3 +17,5 @@ data: Units__ScoutTrainingSeconds: "60" Buildings__LogCampWoodPerMinute: "5" Buildings__LogCampBuildSeconds: "60" + Buildings__MineOrePerMinute: "5" + Buildings__MineBuildSeconds: "60"