From 194dae43da057a050fc738a38bad008150647a74 Mon Sep 17 00:00:00 2001 From: hz Date: Wed, 20 May 2026 04:09:32 -0500 Subject: [PATCH] Updating peasants --- .../scenes/game/hex_grid_screen.gd | 146 ++++++++++++++++- .../scenes/game/hex_grid_view.gd | 81 +++++++++- .../Controllers/LocationsController.cs | 32 ++++ microservices/LocationsApi/Models/Dto.cs | 28 ++++ microservices/LocationsApi/Models/Location.cs | 5 + .../LocationsApi/Models/LocationIntel.cs | 5 + microservices/LocationsApi/Models/UnitDto.cs | 1 + .../Services/GameSettingsService.cs | 3 + .../LocationsApi/Services/LocationService.cs | 132 +++++++++++++-- .../LocationsApi/Services/ResourceService.cs | 152 ++++++++++++++---- .../Services/UnitTrainingService.cs | 28 +++- microservices/LocationsApi/appsettings.json | 3 + microservices/LocationsApi/k8s/configmap.yaml | 3 + 13 files changed, 557 insertions(+), 62 deletions(-) diff --git a/game/territory-autobattler/scenes/game/hex_grid_screen.gd b/game/territory-autobattler/scenes/game/hex_grid_screen.gd index 9b60290..42e8738 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_screen.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_screen.gd @@ -16,6 +16,7 @@ var resources_request: HTTPRequest var units_request: HTTPRequest var train_unit_request: HTTPRequest var move_units_request: HTTPRequest +var worker_request: HTTPRequest var claim_request: HTTPRequest var build_request: HTTPRequest var refresh_request: HTTPRequest @@ -32,6 +33,7 @@ var resource_text := "Gold: --\nWood: --\nOre: --\nFood: --" var gold_tick_anchor_unix := 0.0 var gold_tick_seconds := 60.0 var train_button: Button +var train_peasant_button: Button var train_archer_button: Button var unit_label: Label var training_box: VBoxContainer @@ -57,6 +59,8 @@ var build_farm_button: Button var build_barracks_button: Button var unit_action_popup: PopupPanel var unit_action_move_button: Button +var unit_action_assign_button: Button +var unit_action_deassign_button: Button var selected_q := 0 var selected_r := 0 var selected_can_claim := false @@ -68,6 +72,10 @@ var selected_can_train_archer := false var selected_is_home := false var completed_scout_count := 0 var completed_archer_count := 0 +var completed_peasant_count := 0 +var selected_peasant_id := "" +var selected_peasant_q := 9999 +var selected_peasant_r := 9999 var selected_archer_q := 9999 var selected_archer_r := 9999 var selected_archer_count := 0 @@ -218,6 +226,11 @@ func _build_ui() -> void: train_button.pressed.connect(_train_scout) action_buttons.add_child(train_button) + train_peasant_button = _small_button("Peasant 25g") + train_peasant_button.visible = false + train_peasant_button.pressed.connect(_train_peasant) + action_buttons.add_child(train_peasant_button) + train_archer_button = _small_button("Archer 100g 100w") train_archer_button.visible = false train_archer_button.pressed.connect(_train_archer) @@ -279,6 +292,10 @@ func _build_ui() -> void: move_units_request.request_completed.connect(_on_move_units_request_completed) add_child(move_units_request) + worker_request = HTTPRequest.new() + worker_request.request_completed.connect(_on_worker_request_completed) + add_child(worker_request) + claim_request = HTTPRequest.new() claim_request.request_completed.connect(_on_claim_request_completed) add_child(claim_request) @@ -441,10 +458,24 @@ func _build_unit_action_menu() -> void: margin.add_theme_constant_override("margin_bottom", 8) panel.add_child(margin) + var actions := VBoxContainer.new() + actions.add_theme_constant_override("separation", 6) + margin.add_child(actions) + unit_action_move_button = _small_button("Move") unit_action_move_button.custom_minimum_size = Vector2(92, 34) unit_action_move_button.pressed.connect(_confirm_unit_move) - margin.add_child(unit_action_move_button) + actions.add_child(unit_action_move_button) + + unit_action_assign_button = _small_button("Assign") + unit_action_assign_button.custom_minimum_size = Vector2(92, 34) + unit_action_assign_button.pressed.connect(_confirm_worker_assign) + actions.add_child(unit_action_assign_button) + + unit_action_deassign_button = _small_button("Deassign") + unit_action_deassign_button.custom_minimum_size = Vector2(92, 34) + unit_action_deassign_button.pressed.connect(_confirm_worker_deassign) + actions.add_child(unit_action_deassign_button) func _on_tile_hovered(q: int, r: int, terrain: String) -> void: @@ -475,12 +506,26 @@ func _handle_unit_selection_or_move(q: int, r: int) -> bool: return false var archer_count: int = grid_view.get_archer_count_at(q, r) + var peasant_id: String = grid_view.get_peasant_id_at(q, r) + if not peasant_id.is_empty(): + if unit_action_popup: + unit_action_popup.hide() + selected_peasant_id = peasant_id + selected_peasant_q = q + selected_peasant_r = r + selected_archer_count = 0 + grid_view.set_selected_unit_tile(q, r) + if coordinate_label: + coordinate_label.text = "Selected Peasant at q:%d r:%d. Right-click a building to assign work." % [q, r] + return true + if archer_count > 0: if unit_action_popup: unit_action_popup.hide() selected_archer_q = q selected_archer_r = r selected_archer_count = archer_count + selected_peasant_id = "" grid_view.set_selected_unit_tile(q, r) if coordinate_label: coordinate_label.text = "Selected Archer squad: %d at q:%d r:%d. Right-click an adjacent visible tile for orders." % [archer_count, q, r] @@ -492,17 +537,20 @@ func _handle_unit_selection_or_move(q: int, r: int) -> bool: func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position: Vector2) -> void: if not grid_view: return - if selected_archer_count <= 0: + var location: Dictionary = grid_view.get_visible_location(q, r) + var has_building := _location_has_completed_building(location) + var has_worker := not _nullable_string(location.get("workerUnitId", "")).is_empty() + if selected_archer_count <= 0 and selected_peasant_id.is_empty() and not has_worker: if unit_action_popup: unit_action_popup.hide() if coordinate_label: - coordinate_label.text = "Select an Archer squad before giving orders." + coordinate_label.text = "Select a unit or right-click a staffed building." return - if q == selected_archer_q and r == selected_archer_r: + if selected_archer_count > 0 and q == selected_archer_q and r == selected_archer_r: if coordinate_label: coordinate_label.text = "Archer squad is already on that tile." return - if not grid_view.is_neighbor(selected_archer_q, selected_archer_r, q, r): + if selected_archer_count > 0 and not grid_view.is_neighbor(selected_archer_q, selected_archer_r, q, r): if coordinate_label: coordinate_label.text = "Archers can only move to an adjacent tile." return @@ -514,9 +562,12 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position pending_move_q = q pending_move_r = r if coordinate_label: - coordinate_label.text = "Orders for Archer squad: q:%d r:%d." % [q, r] + coordinate_label.text = "Orders for q:%d r:%d." % [q, r] if unit_action_popup: - var popup_size := Vector2i(116, 52) + unit_action_move_button.visible = selected_archer_count > 0 + unit_action_assign_button.visible = not selected_peasant_id.is_empty() and has_building and not has_worker + unit_action_deassign_button.visible = has_worker + var popup_size := Vector2i(116, 132) var visible_rect := get_viewport().get_visible_rect() var max_x := int(max(0.0, visible_rect.size.x - popup_size.x - 8.0)) var max_y := int(max(0.0, visible_rect.size.y - popup_size.y - 8.0)) @@ -532,6 +583,30 @@ func _confirm_unit_move() -> void: _move_selected_archers(pending_move_q, pending_move_r) +func _confirm_worker_assign() -> void: + if unit_action_popup: + unit_action_popup.hide() + if selected_peasant_id.is_empty() or pending_move_q == 9999: + return + _request_worker_action("assign-worker", { "q": pending_move_q, "r": pending_move_r, "unitId": selected_peasant_id }) + + +func _confirm_worker_deassign() -> void: + if unit_action_popup: + unit_action_popup.hide() + if pending_move_q == 9999: + return + _request_worker_action("deassign-worker", { "q": pending_move_q, "r": pending_move_r }) + + +func _location_has_completed_building(location: Dictionary) -> bool: + if location.is_empty(): + return false + var building_type: String = _nullable_string(location.get("buildingType", "")) + var building_status: String = _nullable_string(location.get("buildingStatus", "")) + return not building_type.is_empty() and (building_status == "Built" or building_status.is_empty()) + + func _can_build_log_camp_on_selected_tile() -> bool: return _can_build_on_selected_tile("Forest") @@ -577,6 +652,9 @@ func _update_action_buttons() -> void: if train_button: train_button.visible = selected_is_home train_button.disabled = false + if train_peasant_button: + train_peasant_button.visible = selected_is_home + train_peasant_button.disabled = false if train_archer_button: train_archer_button.visible = selected_can_train_archer train_archer_button.disabled = false @@ -601,6 +679,9 @@ func _clear_selected_archers() -> void: selected_archer_q = 9999 selected_archer_r = 9999 selected_archer_count = 0 + selected_peasant_id = "" + selected_peasant_q = 9999 + selected_peasant_r = 9999 pending_move_q = 9999 pending_move_r = 9999 if unit_action_popup: @@ -952,6 +1033,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack completed_scout_count = int(units.get("completedScouts", 0)) completed_archer_count = int(units.get("completedArchers", 0)) + completed_peasant_count = int(units.get("completedPeasants", 0)) if grid_view: grid_view.set_completed_archers(completed_archer_count) var unit_instances: Array = [] @@ -959,7 +1041,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack if typeof(raw_units) == TYPE_ARRAY: unit_instances = raw_units grid_view.set_unit_instances(unit_instances) - unit_label.text = "Scouts: %d | Archers: %d | Queue: %d" % [completed_scout_count, completed_archer_count, training.size()] + unit_label.text = "Scouts: %d | Peasants: %d | Archers: %d | Queue: %d" % [completed_scout_count, completed_peasant_count, completed_archer_count, training.size()] _update_action_buttons() if training.is_empty(): @@ -985,6 +1067,14 @@ func _train_scout() -> void: _request_unit_training("Scout") +func _train_peasant() -> void: + if not selected_is_home: + coordinate_label.text = "Select your town hall to train Peasants." + return + + _request_unit_training("Peasant") + + func _train_archer() -> void: if not selected_can_train_archer: coordinate_label.text = "Select a completed Barracks to train Archers." @@ -1004,6 +1094,8 @@ func _request_unit_training(unit_type: String) -> void: if unit_type == "Archer": train_archer_button.disabled = true + elif unit_type == "Peasant": + train_peasant_button.disabled = true else: train_button.disabled = true coordinate_label.text = "Starting %s training..." % unit_type @@ -1106,6 +1198,42 @@ func _on_move_units_request_completed(result: int, response_code: int, _headers: _load_visible_locations() +func _request_worker_action(endpoint: String, body: Dictionary) -> void: + var access_token: String = str(session.get("accessToken", "")) + if access_token.is_empty(): + coordinate_label.text = "No auth token available. Worker assignment failed." + return + if worker_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: + return + + var error: int = worker_request.request( + LOCATIONS_BASE_URL + "/api/locations/" + endpoint, + PackedStringArray([ + "Content-Type: application/json", + "Authorization: Bearer %s" % access_token + ]), + HTTPClient.METHOD_POST, + JSON.stringify(body) + ) + if error != OK: + coordinate_label.text = "Could not contact the worker assignment service." + + +func _on_worker_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 = "Worker request failed." + return + if response_code < 200 or response_code >= 300: + coordinate_label.text = "Worker assignment failed: %s" % text + _refresh_player_state() + return + + _clear_selected_archers() + coordinate_label.text = "Worker assignment updated." + _load_visible_locations() + + func _claim_selected_tile() -> void: if not selected_can_claim: coordinate_label.text = "Select a visible unclaimed tile to send a scout." @@ -1393,6 +1521,8 @@ func _unit_display_name(unit_type: String) -> String: return "Scout" "Archer": return "Archer" + "Peasant": + return "Peasant" _: return "Training" diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index 504a903..5dc910a 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -75,7 +75,7 @@ func _gui_input(event: InputEvent) -> void: if event.pressed: drag_distance = 0.0 if not event.pressed and drag_distance < 6.0 and hovered_tile.x != 9999: - var clicked_unit_tile := _archer_tile_at(event.position) + var clicked_unit_tile := _unit_tile_at(event.position) selected_tile = clicked_unit_tile if clicked_unit_tile.x != 9999 else hovered_tile var data: Dictionary = _tile_data(selected_tile.x, selected_tile.y) var key: String = _tile_key(selected_tile.x, selected_tile.y) @@ -194,6 +194,9 @@ func _draw_tile(tile: Dictionary) -> void: var archer_count := _archer_count_at(q, r) if archer_count > 0: _draw_archer_unit_marker(center, height, archer_count) + var peasant_count := _peasant_count_at(q, r) + if peasant_count > 0: + _draw_peasant_unit_marker(center, height, peasant_count) func _draw_control_regions(sorted_tiles: Array) -> void: @@ -542,6 +545,56 @@ func _archer_tile_at(mouse_position: Vector2) -> Vector2i: return closest +func _unit_tile_at(mouse_position: Vector2) -> Vector2i: + var peasant_tile := _peasant_tile_at(mouse_position) + if peasant_tile.x != 9999: + return peasant_tile + return _archer_tile_at(mouse_position) + + +func _peasant_marker_center(center: Vector2, height: float) -> Vector2: + return center + Vector2(-16.0 * zoom, -height * zoom - 12.0 * zoom) + + +func _peasant_tile_at(mouse_position: Vector2) -> Vector2i: + var closest := Vector2i(9999, 9999) + var closest_distance := 999999.0 + for tile in tiles: + var q: int = int(tile["q"]) + var r: int = int(tile["r"]) + if _peasant_count_at(q, r) <= 0: + continue + + var center: Vector2 = _tile_center(q, r) + var height: float = float(tile["height"]) + var distance: float = _peasant_marker_center(center, height).distance_to(mouse_position) + if distance < closest_distance: + closest_distance = distance + closest = Vector2i(q, r) + + if closest_distance > 24.0 * zoom: + return Vector2i(9999, 9999) + return closest + + +func _draw_peasant_unit_marker(center: Vector2, height: float, count: int) -> void: + var marker_center: Vector2 = _peasant_marker_center(center, height) + var marker_scale: float = zoom + var tunic_color := Color(0.54, 0.42, 0.24) + var skin_color := Color(0.78, 0.58, 0.38) + var tool_color := Color(0.55, 0.42, 0.22) + var outline_color := Color(0.05, 0.04, 0.03) + + draw_circle(marker_center + Vector2(1, 2) * marker_scale, 9.0 * marker_scale, Color(0.01, 0.02, 0.02, 0.45)) + draw_rect(Rect2(marker_center + Vector2(-5, -1) * marker_scale, Vector2(10, 12) * marker_scale), tunic_color, true) + draw_circle(marker_center + Vector2(0, -7) * marker_scale, 4.5 * marker_scale, skin_color) + draw_line(marker_center + Vector2(7, -8) * marker_scale, marker_center + Vector2(13, 8) * marker_scale, tool_color, 2.0 * marker_scale) + draw_line(marker_center + Vector2(10, -2) * marker_scale, marker_center + Vector2(16, -4) * marker_scale, tool_color, 1.5 * marker_scale) + draw_rect(Rect2(marker_center + Vector2(-5, -1) * marker_scale, Vector2(10, 12) * marker_scale), outline_color, false, 1.0 * marker_scale) + if count > 1: + draw_string(ThemeDB.fallback_font, marker_center + Vector2(7, 10) * marker_scale, str(count), HORIZONTAL_ALIGNMENT_LEFT, -1, int(9 * marker_scale), Color(0.94, 0.86, 0.6)) + + func _tile_data(q: int, r: int) -> Dictionary: for tile in tiles: if int(tile["q"]) == q and int(tile["r"]) == r: @@ -633,6 +686,15 @@ func get_archer_count_at(q: int, r: int) -> int: return _archer_count_at(q, r) +func get_peasant_id_at(q: int, r: int) -> String: + for unit in unit_instances: + if str(unit.get("unitType", "")) != "Peasant" or str(unit.get("status", "")) != "Idle": + continue + if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r: + return str(unit.get("id", "")) + return "" + + func is_visible_non_rival_tile(q: int, r: int) -> bool: var key := _tile_key(q, r) return visible_tiles.has(key) and not rival_tiles.has(key) @@ -691,6 +753,9 @@ func _tile_display_label(key: String, data: Dictionary) -> String: var terrain: String = str(data.get("terrain", "Open")) var building_type: String = _nullable_string(data.get("buildingType", "")) var building_status: String = _nullable_string(data.get("buildingStatus", "")) + var worker_unit_id: String = _nullable_string(data.get("workerUnitId", "")) + var stored_resource: int = int(data.get("storedResourceAmount", 0)) + var stored_capacity: int = int(data.get("storedResourceCapacity", 0)) 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" @@ -700,6 +765,10 @@ func _tile_display_label(key: String, data: Dictionary) -> String: 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" + if not building_label.is_empty() and not worker_unit_id.is_empty(): + building_label += " staffed" + if not building_label.is_empty() and stored_capacity > 0: + building_label += " %d/%d" % [stored_resource, stored_capacity] var archer_count := _archer_count_at(int(data.get("q", 0)), int(data.get("r", 0))) if archer_count > 0: building_label += " | Archers: %d" % archer_count @@ -749,6 +818,16 @@ func _archer_count_at(q: int, r: int) -> int: return count +func _peasant_count_at(q: int, r: int) -> int: + var count := 0 + for unit in unit_instances: + if str(unit.get("unitType", "")) != "Peasant" or str(unit.get("status", "")) != "Idle": + continue + if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r: + count += 1 + return count + + func _owner_username(location: Dictionary) -> String: var owner_value: Variant = location.get("ownerUsername", "") if owner_value == null: diff --git a/microservices/LocationsApi/Controllers/LocationsController.cs b/microservices/LocationsApi/Controllers/LocationsController.cs index 7b1bc93..dfc0467 100644 --- a/microservices/LocationsApi/Controllers/LocationsController.cs +++ b/microservices/LocationsApi/Controllers/LocationsController.cs @@ -148,6 +148,38 @@ public class LocationsController : ControllerBase } } + [Authorize(Roles = "USER,SUPER")] + [HttpPost("assign-worker")] + public async Task> AssignWorker([FromBody] AssignWorkerRequest request) + { + var username = User.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(username)) + { + return Unauthorized(); + } + + var location = await _locations.AssignWorkerAsync(username, request); + return location is null + ? BadRequest("Select an idle Peasant and a controlled completed building without a worker.") + : Ok(LocationResponse.FromLocation(location)); + } + + [Authorize(Roles = "USER,SUPER")] + [HttpPost("deassign-worker")] + public async Task> DeassignWorker([FromBody] DeassignWorkerRequest request) + { + var username = User.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(username)) + { + return Unauthorized(); + } + + var location = await _locations.DeassignWorkerAsync(username, request); + return location is null + ? BadRequest("Select a controlled building with an assigned worker.") + : Ok(LocationResponse.FromLocation(location)); + } + [Authorize(Roles = "USER,SUPER")] [HttpGet("home")] public async Task> GetHome() diff --git a/microservices/LocationsApi/Models/Dto.cs b/microservices/LocationsApi/Models/Dto.cs index d69ae4c..9fb5eec 100644 --- a/microservices/LocationsApi/Models/Dto.cs +++ b/microservices/LocationsApi/Models/Dto.cs @@ -22,6 +22,19 @@ public class BuildLocationRequest public string BuildingType { get; set; } = "LogCamp"; } +public class AssignWorkerRequest +{ + public int Q { get; set; } + public int R { get; set; } + public string UnitId { get; set; } = ""; +} + +public class DeassignWorkerRequest +{ + public int Q { get; set; } + public int R { get; set; } +} + public class LocationResponse { public string Id { get; set; } = ""; @@ -37,6 +50,11 @@ public class LocationResponse public DateTime? BuildingStartedAtUtc { get; set; } public DateTime? BuildingCompletesAtUtc { get; set; } public DateTime? BuildingBuiltAtUtc { get; set; } + public string? WorkerUnitId { get; set; } + public string? WorkerUnitType { get; set; } + public int StoredResourceAmount { get; set; } + public int StoredResourceCapacity { get; set; } + public DateTime? LastResourceGeneratedAtUtc { get; set; } public DateTime? HomeAssignedAtUtc { get; set; } public DateTime UpdatedAtUtc { get; set; } public bool IsVisible { get; set; } = true; @@ -58,6 +76,11 @@ public class LocationResponse BuildingStartedAtUtc = revealDetails ? location.BuildingStartedAtUtc : null, BuildingCompletesAtUtc = revealDetails ? location.BuildingCompletesAtUtc : null, BuildingBuiltAtUtc = revealDetails ? location.BuildingBuiltAtUtc : null, + WorkerUnitId = revealDetails ? location.WorkerUnitId : null, + WorkerUnitType = revealDetails ? location.WorkerUnitType : null, + StoredResourceAmount = revealDetails ? location.StoredResourceAmount : 0, + StoredResourceCapacity = revealDetails ? location.StoredResourceCapacity : 0, + LastResourceGeneratedAtUtc = revealDetails ? location.LastResourceGeneratedAtUtc : null, HomeAssignedAtUtc = location.HomeAssignedAtUtc, UpdatedAtUtc = location.UpdatedAtUtc, IsVisible = true @@ -80,6 +103,11 @@ public class LocationResponse BuildingStartedAtUtc = intel.BuildingStartedAtUtc, BuildingCompletesAtUtc = intel.BuildingCompletesAtUtc, BuildingBuiltAtUtc = intel.BuildingBuiltAtUtc, + WorkerUnitId = intel.WorkerUnitId, + WorkerUnitType = intel.WorkerUnitType, + StoredResourceAmount = intel.StoredResourceAmount, + StoredResourceCapacity = intel.StoredResourceCapacity, + LastResourceGeneratedAtUtc = intel.LastResourceGeneratedAtUtc, HomeAssignedAtUtc = intel.HomeAssignedAtUtc, UpdatedAtUtc = intel.ObservedAtUtc, IsVisible = true diff --git a/microservices/LocationsApi/Models/Location.cs b/microservices/LocationsApi/Models/Location.cs index 362f79e..ca74df9 100644 --- a/microservices/LocationsApi/Models/Location.cs +++ b/microservices/LocationsApi/Models/Location.cs @@ -21,6 +21,11 @@ public class Location public DateTime? BuildingStartedAtUtc { get; set; } public DateTime? BuildingCompletesAtUtc { get; set; } public DateTime? BuildingBuiltAtUtc { get; set; } + public string? WorkerUnitId { get; set; } + public string? WorkerUnitType { get; set; } + public int StoredResourceAmount { get; set; } + public int StoredResourceCapacity { get; set; } = 10; + public DateTime? LastResourceGeneratedAtUtc { get; set; } public DateTime? HomeAssignedAtUtc { get; set; } public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow; diff --git a/microservices/LocationsApi/Models/LocationIntel.cs b/microservices/LocationsApi/Models/LocationIntel.cs index 59c6396..7a513f6 100644 --- a/microservices/LocationsApi/Models/LocationIntel.cs +++ b/microservices/LocationsApi/Models/LocationIntel.cs @@ -22,6 +22,11 @@ public class LocationIntel public DateTime? BuildingStartedAtUtc { get; set; } public DateTime? BuildingCompletesAtUtc { get; set; } public DateTime? BuildingBuiltAtUtc { get; set; } + public string? WorkerUnitId { get; set; } + public string? WorkerUnitType { get; set; } + public int StoredResourceAmount { get; set; } + public int StoredResourceCapacity { get; set; } = 10; + public DateTime? LastResourceGeneratedAtUtc { get; set; } public DateTime? HomeAssignedAtUtc { get; set; } public DateTime ObservedAtUtc { get; set; } = DateTime.UtcNow; public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; diff --git a/microservices/LocationsApi/Models/UnitDto.cs b/microservices/LocationsApi/Models/UnitDto.cs index 00f3936..52e3f0e 100644 --- a/microservices/LocationsApi/Models/UnitDto.cs +++ b/microservices/LocationsApi/Models/UnitDto.cs @@ -76,6 +76,7 @@ public class PlayerUnitsResponse { public int CompletedScouts { get; set; } public int CompletedArchers { get; set; } + public int CompletedPeasants { get; set; } public List Training { get; set; } = []; public List Units { get; set; } = []; } diff --git a/microservices/LocationsApi/Services/GameSettingsService.cs b/microservices/LocationsApi/Services/GameSettingsService.cs index ead5feb..f4c9dd7 100644 --- a/microservices/LocationsApi/Services/GameSettingsService.cs +++ b/microservices/LocationsApi/Services/GameSettingsService.cs @@ -7,9 +7,11 @@ public class GameSettingsService { public const string ScoutTrainingSeconds = "ScoutTrainingSeconds"; public const string ArcherTrainingSeconds = "ArcherTrainingSeconds"; + public const string PeasantTrainingSeconds = "PeasantTrainingSeconds"; public const string ScoutGoldCost = "ScoutGoldCost"; public const string ArcherGoldCost = "ArcherGoldCost"; public const string ArcherWoodCost = "ArcherWoodCost"; + public const string PeasantGoldCost = "PeasantGoldCost"; public const string LogCampBuildSeconds = "LogCampBuildSeconds"; public const string MineBuildSeconds = "MineBuildSeconds"; public const string BarracksBuildSeconds = "BarracksBuildSeconds"; @@ -20,6 +22,7 @@ public class GameSettingsService public const string LogCampWoodPerMinute = "LogCampWoodPerMinute"; public const string MineOrePerMinute = "MineOrePerMinute"; public const string FarmFoodPerMinute = "FarmFoodPerMinute"; + public const string ProductionBuildingResourceCapacity = "ProductionBuildingResourceCapacity"; public const string StartingGold = "StartingGold"; public const string HeadquartersGoldPerMinute = "HeadquartersGoldPerMinute"; public const string TownHallGoldCapacity = "TownHallGoldCapacity"; diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index 4055442..74e30f9 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -263,6 +263,7 @@ public class LocationService var update = Builders.Update .Set(x => x.BuildingStatus, "Built") .Set(x => x.BuildingBuiltAtUtc, now) + .Set(x => x.StoredResourceCapacity, 10) .Set(x => x.UpdatedAtUtc, now); var updated = await _locations.FindOneAndUpdateAsync( @@ -275,25 +276,120 @@ public class LocationService if (updated is not null) { - if (updated.BuildingType == "LogCamp") - { - var woodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.LogCampWoodPerMinute, _logCampWoodPerMinute); - await _resources.AddWoodGenerationAsync(username, woodPerMinute); - } - else if (updated.BuildingType == "Mine") - { - 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); - } + await RecalculateWorkerGenerationAsync(username); } } } + public async Task AssignWorkerAsync(string username, AssignWorkerRequest request) + { + var now = DateTime.UtcNow; + var location = await GetAsync(request.Q, request.R); + if (location is null || + !string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) || + string.IsNullOrWhiteSpace(location.BuildingType) || + location.BuildingStatus == "Building" || + !string.IsNullOrWhiteSpace(location.WorkerUnitId)) + { + return null; + } + + var unit = await _units.Find(x => + x.Id == request.UnitId && + x.Username == username && + x.UnitType == "Peasant" && + x.Status == "Idle") + .FirstOrDefaultAsync(); + if (unit is null) + { + return null; + } + + var claimed = await _units.UpdateOneAsync( + x => x.Id == unit.Id && x.Status == "Idle", + Builders.Update + .Set(x => x.Q, request.Q) + .Set(x => x.R, request.R) + .Set(x => x.Status, "Working") + .Set(x => x.UpdatedAtUtc, now)); + if (claimed.ModifiedCount == 0) + { + return null; + } + + var updated = await _locations.FindOneAndUpdateAsync( + x => x.Id == location.Id && (x.WorkerUnitId == null || x.WorkerUnitId == ""), + Builders.Update + .Set(x => x.WorkerUnitId, unit.Id) + .Set(x => x.WorkerUnitType, "Peasant") + .Set(x => x.LastResourceGeneratedAtUtc, now) + .Set(x => x.UpdatedAtUtc, now), + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + + if (updated is null) + { + await _units.UpdateOneAsync( + x => x.Id == unit.Id, + Builders.Update + .Set(x => x.Status, "Idle") + .Set(x => x.UpdatedAtUtc, DateTime.UtcNow)); + return null; + } + + await RecalculateWorkerGenerationAsync(username); + return updated; + } + + public async Task DeassignWorkerAsync(string username, DeassignWorkerRequest request) + { + var now = DateTime.UtcNow; + var location = await GetAsync(request.Q, request.R); + if (location is null || + !string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) || + string.IsNullOrWhiteSpace(location.WorkerUnitId)) + { + return null; + } + + await _units.UpdateOneAsync( + x => x.Id == location.WorkerUnitId && x.Username == username && x.Status == "Working", + Builders.Update + .Set(x => x.Q, request.Q) + .Set(x => x.R, request.R) + .Set(x => x.Status, "Idle") + .Set(x => x.UpdatedAtUtc, now)); + + var updated = await _locations.FindOneAndUpdateAsync( + x => x.Id == location.Id, + Builders.Update + .Unset(x => x.WorkerUnitId) + .Unset(x => x.WorkerUnitType) + .Set(x => x.UpdatedAtUtc, now), + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + + await RecalculateWorkerGenerationAsync(username); + return updated; + } + + private async Task RecalculateWorkerGenerationAsync(string username) + { + var built = await _locations.Find(x => + x.OwnerUsername == username && + !string.IsNullOrWhiteSpace(x.WorkerUnitId) && + (x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") && + (x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Farm")) + .ToListAsync(); + + var woodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.LogCampWoodPerMinute, _logCampWoodPerMinute); + var orePerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.MineOrePerMinute, _mineOrePerMinute); + var foodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, _farmFoodPerMinute); + await _resources.SetBuildingGenerationAsync( + username, + built.Count(x => x.BuildingType == "LogCamp") * woodPerMinute, + built.Count(x => x.BuildingType == "Mine") * orePerMinute, + built.Count(x => x.BuildingType == "Farm") * foodPerMinute); + } + public async Task GetHomeAsync(string username) => await _locations.Find(x => x.OwnerUsername == username && x.Kind == "Home").FirstOrDefaultAsync(); @@ -337,6 +433,7 @@ public class LocationService public async Task> GetVisibleAsync(string username, int? visibilityDepth = null) { await CompleteFinishedBuildingsAsync(username); + await RecalculateWorkerGenerationAsync(username); var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync(); if (ownedLocations.Count == 0) @@ -551,6 +648,11 @@ public class LocationService .Set(x => x.BuildingStartedAtUtc, location.BuildingStartedAtUtc) .Set(x => x.BuildingCompletesAtUtc, location.BuildingCompletesAtUtc) .Set(x => x.BuildingBuiltAtUtc, location.BuildingBuiltAtUtc) + .Set(x => x.WorkerUnitId, location.WorkerUnitId) + .Set(x => x.WorkerUnitType, location.WorkerUnitType) + .Set(x => x.StoredResourceAmount, location.StoredResourceAmount) + .Set(x => x.StoredResourceCapacity, location.StoredResourceCapacity) + .Set(x => x.LastResourceGeneratedAtUtc, location.LastResourceGeneratedAtUtc) .Set(x => x.HomeAssignedAtUtc, location.HomeAssignedAtUtc) .Set(x => x.ObservedAtUtc, now) .Set(x => x.UpdatedAtUtc, now); diff --git a/microservices/LocationsApi/Services/ResourceService.cs b/microservices/LocationsApi/Services/ResourceService.cs index 4d92c46..56cb2db 100644 --- a/microservices/LocationsApi/Services/ResourceService.cs +++ b/microservices/LocationsApi/Services/ResourceService.cs @@ -6,6 +6,7 @@ namespace LocationsApi.Services; public class ResourceService { private readonly IMongoCollection _resources; + private readonly IMongoCollection _locations; private readonly GameSettingsService _settings; private readonly int _startingGold; private readonly int _headquartersGoldPerMinute; @@ -13,6 +14,7 @@ public class ResourceService private readonly int _townHallWoodCapacity; private readonly int _townHallOreCapacity; private readonly int _townHallFoodCapacity; + private readonly int _productionBuildingResourceCapacity; public ResourceService(IConfiguration cfg, GameSettingsService settings) { @@ -21,6 +23,7 @@ public class ResourceService var client = new MongoClient(connectionString); var database = client.GetDatabase(databaseName); _resources = database.GetCollection("PlayerResources"); + _locations = database.GetCollection("Locations"); _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) @@ -38,6 +41,9 @@ public class ResourceService _townHallFoodCapacity = int.TryParse(cfg["Resources:TownHallFoodCapacity"], out var foodCapacity) ? Math.Max(0, foodCapacity) : 100; + _productionBuildingResourceCapacity = int.TryParse(cfg["Buildings:ProductionBuildingResourceCapacity"], out var productionBuildingResourceCapacity) + ? Math.Max(0, productionBuildingResourceCapacity) + : 10; _resources.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(x => x.Username), @@ -85,7 +91,8 @@ public class ResourceService public async Task GetAsync(string username) { - return await EnsureAsync(username); + var resources = await EnsureAsync(username); + return await ApplyProductionBuildingDeliveryAsync(resources); } public async Task TrySpendGoldAsync(string username, int amount) @@ -207,6 +214,33 @@ public class ResourceService await _resources.UpdateOneAsync(x => x.Username == username, update); } + public async Task SetBuildingGenerationAsync(string username, int woodPerMinute, int orePerMinute, int foodPerMinute) + { + await EnsureAsync(username); + var now = DateTime.UtcNow; + var update = Builders.Update + .Set(x => x.WoodPerMinute, Math.Max(0, woodPerMinute)) + .Set(x => x.OrePerMinute, Math.Max(0, orePerMinute)) + .Set(x => x.FoodPerMinute, Math.Max(0, foodPerMinute)) + .Set(x => x.UpdatedAtUtc, now); + + if (woodPerMinute > 0) + { + update = update.Set(x => x.LastWoodGeneratedAtUtc, now); + } + if (orePerMinute > 0) + { + update = update.Set(x => x.LastOreGeneratedAtUtc, now); + } + if (foodPerMinute > 0) + { + 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; @@ -218,22 +252,13 @@ public class ResourceService var goldElapsedMinutes = goldPerMinute <= 0 ? 0 : (int)Math.Floor((now - resources.LastGoldGeneratedAtUtc).TotalMinutes); - 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); - 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 && foodElapsedMinutes <= 0 && !isOverCapacity) + if (goldElapsedMinutes <= 0 && !isOverCapacity) { return resources; } @@ -263,30 +288,6 @@ public class ResourceService .Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes)); } - if (woodElapsedMinutes > 0) - { - var generatedWood = Math.Min(woodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute)); - update = update - .Set(x => x.Wood, generatedWood) - .Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes)); - } - - if (oreElapsedMinutes > 0) - { - 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, @@ -295,4 +296,85 @@ public class ResourceService ReturnDocument = ReturnDocument.After }); } + + private async Task ApplyProductionBuildingDeliveryAsync(PlayerResources resources) + { + var now = DateTime.UtcNow; + 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 buildingCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.ProductionBuildingResourceCapacity, _productionBuildingResourceCapacity); + var logCampWoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.LogCampWoodPerMinute, 5); + var mineOrePerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.MineOrePerMinute, 5); + var farmFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, 5); + + var buildings = await _locations.Find(x => + x.OwnerUsername == resources.Username && + !string.IsNullOrWhiteSpace(x.WorkerUnitId) && + (x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") && + (x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Farm")) + .ToListAsync(); + + var wood = Math.Min(resources.Wood, woodCapacity); + var ore = Math.Min(resources.Ore, oreCapacity); + var food = Math.Min(resources.Food, foodCapacity); + + foreach (var building in buildings) + { + var rate = building.BuildingType switch + { + "LogCamp" => logCampWoodPerMinute, + "Mine" => mineOrePerMinute, + "Farm" => farmFoodPerMinute, + _ => 0 + }; + var lastGenerated = building.LastResourceGeneratedAtUtc ?? building.BuildingBuiltAtUtc ?? now; + var elapsedMinutes = rate <= 0 ? 0 : (int)Math.Floor((now - lastGenerated).TotalMinutes); + var stored = Math.Min(buildingCapacity, Math.Max(0, building.StoredResourceAmount) + (elapsedMinutes * rate)); + + var delivered = 0; + if (building.BuildingType == "LogCamp") + { + delivered = Math.Min(stored, Math.Max(0, woodCapacity - wood)); + wood += delivered; + } + else if (building.BuildingType == "Mine") + { + delivered = Math.Min(stored, Math.Max(0, oreCapacity - ore)); + ore += delivered; + } + else if (building.BuildingType == "Farm") + { + delivered = Math.Min(stored, Math.Max(0, foodCapacity - food)); + food += delivered; + } + + var update = Builders.Update + .Set(x => x.StoredResourceAmount, stored - delivered) + .Set(x => x.StoredResourceCapacity, buildingCapacity) + .Set(x => x.UpdatedAtUtc, now); + + if (elapsedMinutes > 0 || building.LastResourceGeneratedAtUtc is null) + { + update = update.Set(x => x.LastResourceGeneratedAtUtc, lastGenerated.AddMinutes(elapsedMinutes)); + } + + await _locations.UpdateOneAsync(x => x.Id == building.Id, update); + } + + return await _resources.FindOneAndUpdateAsync( + x => x.Username == resources.Username, + Builders.Update + .Set(x => x.Wood, wood) + .Set(x => x.Ore, ore) + .Set(x => x.Food, food) + .Set(x => x.WoodCapacity, woodCapacity) + .Set(x => x.OreCapacity, oreCapacity) + .Set(x => x.FoodCapacity, foodCapacity) + .Set(x => x.UpdatedAtUtc, now), + new FindOneAndUpdateOptions + { + ReturnDocument = ReturnDocument.After + }); + } } diff --git a/microservices/LocationsApi/Services/UnitTrainingService.cs b/microservices/LocationsApi/Services/UnitTrainingService.cs index d0ed4d4..32d31df 100644 --- a/microservices/LocationsApi/Services/UnitTrainingService.cs +++ b/microservices/LocationsApi/Services/UnitTrainingService.cs @@ -7,6 +7,7 @@ public class UnitTrainingService { private const string ScoutUnitType = "Scout"; private const string ArcherUnitType = "Archer"; + private const string PeasantUnitType = "Peasant"; private readonly IMongoCollection _training; private readonly IMongoCollection _units; @@ -16,8 +17,10 @@ public class UnitTrainingService private readonly int _scoutGoldCost; private readonly int _archerGoldCost; private readonly int _archerWoodCost; + private readonly int _peasantGoldCost; private readonly int _defaultScoutTrainingSeconds; private readonly int _defaultArcherTrainingSeconds; + private readonly int _defaultPeasantTrainingSeconds; private readonly int _visibilityDepth; public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings) @@ -34,12 +37,16 @@ public class UnitTrainingService _scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50; _archerGoldCost = int.TryParse(cfg["Units:ArcherGoldCost"], out var archerGoldCost) ? Math.Max(0, archerGoldCost) : 100; _archerWoodCost = int.TryParse(cfg["Units:ArcherWoodCost"], out var archerWoodCost) ? Math.Max(0, archerWoodCost) : 100; + _peasantGoldCost = int.TryParse(cfg["Units:PeasantGoldCost"], out var peasantGoldCost) ? Math.Max(0, peasantGoldCost) : 25; _defaultScoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds) ? Math.Max(1, scoutTrainingSeconds) : 60; _defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds) ? Math.Max(1, archerTrainingSeconds) : 60; + _defaultPeasantTrainingSeconds = int.TryParse(cfg["Units:PeasantTrainingSeconds"], out var peasantTrainingSeconds) + ? Math.Max(1, peasantTrainingSeconds) + : 60; _visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3; _training.Indexes.CreateOne(new CreateIndexModel( @@ -64,6 +71,7 @@ public class UnitTrainingService var allUnits = await _units.Find(x => x.Username == username).ToListAsync(); var completedScouts = allTraining.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed"); var completedArchers = allUnits.Count(x => x.UnitType == ArcherUnitType); + var completedPeasants = allUnits.Count(x => x.UnitType == PeasantUnitType && x.Status == "Idle"); var activeTraining = allTraining .Where(x => x.Status == "Training") .OrderBy(x => x.CompletesAtUtc) @@ -74,6 +82,7 @@ public class UnitTrainingService { CompletedScouts = completedScouts, CompletedArchers = completedArchers, + CompletedPeasants = completedPeasants, Training = activeTraining, Units = allUnits .OrderBy(x => x.Q) @@ -106,10 +115,20 @@ public class UnitTrainingService throw new InvalidOperationException("A completed Barracks is required to train Archers."); } } + else if (normalizedUnitType == PeasantUnitType) + { + origin = await _locations.Find(x => x.OwnerUsername == username && x.Kind == "Home").FirstOrDefaultAsync(); + if (origin is null) + { + throw new InvalidOperationException("A town hall is required to train Peasants."); + } + } var goldCost = normalizedUnitType == ArcherUnitType ? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherGoldCost, _archerGoldCost) - : await _settings.GetNonNegativeIntAsync(GameSettingsService.ScoutGoldCost, _scoutGoldCost); + : normalizedUnitType == PeasantUnitType + ? await _settings.GetNonNegativeIntAsync(GameSettingsService.PeasantGoldCost, _peasantGoldCost) + : await _settings.GetNonNegativeIntAsync(GameSettingsService.ScoutGoldCost, _scoutGoldCost); var woodCost = normalizedUnitType == ArcherUnitType ? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherWoodCost, _archerWoodCost) : 0; @@ -124,6 +143,8 @@ public class UnitTrainingService var now = DateTime.UtcNow; var trainingSeconds = normalizedUnitType == ArcherUnitType ? await _settings.GetIntAsync(GameSettingsService.ArcherTrainingSeconds, _defaultArcherTrainingSeconds) + : normalizedUnitType == PeasantUnitType + ? await _settings.GetIntAsync(GameSettingsService.PeasantTrainingSeconds, _defaultPeasantTrainingSeconds) : await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds); var training = new UnitTraining { @@ -350,7 +371,7 @@ public class UnitTrainingService ReturnDocument = ReturnDocument.After }); - if (completed is null || completed.UnitType != ArcherUnitType) + if (completed is null || (completed.UnitType != ArcherUnitType && completed.UnitType != PeasantUnitType)) { continue; } @@ -358,7 +379,7 @@ public class UnitTrainingService var unit = new UnitInstance { Username = completed.Username, - UnitType = ArcherUnitType, + UnitType = completed.UnitType, Q = completed.OriginQ ?? 0, R = completed.OriginR ?? 0, Status = "Idle", @@ -384,6 +405,7 @@ public class UnitTrainingService { "scout" => ScoutUnitType, "archer" => ArcherUnitType, + "peasant" => PeasantUnitType, _ => null }; } diff --git a/microservices/LocationsApi/appsettings.json b/microservices/LocationsApi/appsettings.json index 47d2b23..a599125 100644 --- a/microservices/LocationsApi/appsettings.json +++ b/microservices/LocationsApi/appsettings.json @@ -30,6 +30,8 @@ "Units": { "ScoutGoldCost": 50, "ScoutTrainingSeconds": 60, + "PeasantGoldCost": 25, + "PeasantTrainingSeconds": 60, "ArcherGoldCost": 100, "ArcherWoodCost": 100, "ArcherTrainingSeconds": 60 @@ -40,6 +42,7 @@ "MineOrePerMinute": 5, "MineBuildSeconds": 60, "FarmFoodPerMinute": 5, + "ProductionBuildingResourceCapacity": 10, "BarracksBuildSeconds": 60, "FarmBuildSeconds": 60, "BarracksGoldCost": 500, diff --git a/microservices/LocationsApi/k8s/configmap.yaml b/microservices/LocationsApi/k8s/configmap.yaml index 0e3bf62..4257b07 100644 --- a/microservices/LocationsApi/k8s/configmap.yaml +++ b/microservices/LocationsApi/k8s/configmap.yaml @@ -19,6 +19,8 @@ data: Resources__TownHallFoodCapacity: "100" Units__ScoutGoldCost: "50" Units__ScoutTrainingSeconds: "60" + Units__PeasantGoldCost: "25" + Units__PeasantTrainingSeconds: "60" Units__ArcherGoldCost: "100" Units__ArcherWoodCost: "100" Units__ArcherTrainingSeconds: "60" @@ -27,6 +29,7 @@ data: Buildings__MineOrePerMinute: "5" Buildings__MineBuildSeconds: "60" Buildings__FarmFoodPerMinute: "5" + Buildings__ProductionBuildingResourceCapacity: "10" Buildings__BarracksBuildSeconds: "60" Buildings__FarmBuildSeconds: "60" Buildings__BarracksGoldCost: "500"