diff --git a/game/territory-autobattler/scenes/game/hex_grid_screen.gd b/game/territory-autobattler/scenes/game/hex_grid_screen.gd index bf685b5..3032465 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_screen.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_screen.gd @@ -2,7 +2,9 @@ extends Control const SESSION_PATH := "user://auth_session.json" const LOCATIONS_BASE_URL := "https://tloc.ranaze.com" +const AUTH_BASE_URL := "https://tauth.ranaze.com" const RESOURCE_REFRESH_SECONDS := 15.0 +const TOKEN_REFRESH_PROMPT_SECONDS := 120.0 const HexGridView := preload("res://scenes/game/hex_grid_view.gd") var coordinate_label: Label @@ -15,21 +17,29 @@ var units_request: HTTPRequest var train_unit_request: HTTPRequest var claim_request: HTTPRequest var build_request: HTTPRequest +var refresh_request: HTTPRequest var resources_timer: Timer +var auth_check_timer: Timer +var refresh_dialog: ConfirmationDialog var session: Dictionary = {} var escape_overlay: Control var escape_panel: PanelContainer var gold_label: Label var tick_bar: ProgressBar var tick_label: Label +var resource_text := "Gold: --\nWood: --\nOre: --" var gold_tick_anchor_unix := 0.0 var gold_tick_seconds := 60.0 var train_button: Button var unit_label: Label +var training_box: VBoxContainer var training_label: Label var training_bar: ProgressBar +var building_box: VBoxContainer var building_label: Label var building_bar: ProgressBar +var progress_panel: PanelContainer +var action_buttons: HBoxContainer var training_started_unix := 0.0 var training_completes_unix := 0.0 var training_completion_refresh_requested := false @@ -40,19 +50,24 @@ var active_building_label := "Build" var claim_button: Button var build_log_camp_button: Button var build_mine_button: Button +var build_barracks_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_can_build_barracks := false var selected_is_home := false var completed_scout_count := 0 +var refresh_prompt_active := false +var refresh_request_active := false func _ready() -> void: session = _load_session() _build_ui() _assign_home_location() + _start_auth_watch() func _process(_delta: float) -> void: @@ -100,10 +115,10 @@ func _build_ui() -> void: top_margin.add_theme_constant_override("margin_bottom", 12) top_bar.add_child(top_margin) - var top_content := HBoxContainer.new() - top_content.alignment = BoxContainer.ALIGNMENT_CENTER - top_content.add_theme_constant_override("separation", 16) - top_margin.add_child(top_content) + var title_row := HBoxContainer.new() + title_row.alignment = BoxContainer.ALIGNMENT_CENTER + title_row.add_theme_constant_override("separation", 16) + top_margin.add_child(title_row) var title := Label.new() title.text = "Territory Map" @@ -112,11 +127,11 @@ func _build_ui() -> void: title.add_theme_color_override("font_color", Color(0.94, 0.96, 0.9)) if title_font: title.add_theme_font_override("font", title_font) - top_content.add_child(title) + title_row.add_child(title) var legend := HBoxContainer.new() legend.add_theme_constant_override("separation", 12) - top_content.add_child(legend) + title_row.add_child(legend) legend.add_child(_legend_item(Color(1.0, 0.76, 0.16), "Home")) legend.add_child(_legend_item(Color(0.16, 0.86, 0.92), "Yours")) @@ -124,38 +139,11 @@ func _build_ui() -> void: legend.add_child(_legend_item(Color(0.38, 0.56, 0.36), "Open")) legend.add_child(_legend_item(Color(0.045, 0.065, 0.065), "Unknown")) - gold_label = Label.new() - gold_label.text = "Gold: --" - gold_label.add_theme_font_size_override("font_size", 15) - gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32)) - top_content.add_child(gold_label) - - var tick_box := VBoxContainer.new() - tick_box.custom_minimum_size = Vector2(130, 0) - tick_box.add_theme_constant_override("separation", 4) - top_content.add_child(tick_box) - - tick_label = Label.new() - tick_label.text = "Next: --" - tick_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - tick_label.add_theme_font_size_override("font_size", 12) - tick_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) - tick_box.add_child(tick_label) - - tick_bar = ProgressBar.new() - tick_bar.custom_minimum_size = Vector2(130, 8) - tick_bar.max_value = 1.0 - tick_bar.value = 0.0 - tick_bar.show_percentage = false - tick_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26))) - tick_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.82, 0.65, 0.2), Color(0.9, 0.78, 0.36))) - tick_box.add_child(tick_bar) - var user_label := Label.new() user_label.text = _session_name() user_label.add_theme_font_size_override("font_size", 15) user_label.add_theme_color_override("font_color", Color(0.63, 0.78, 0.75)) - top_content.add_child(user_label) + title_row.add_child(user_label) var map_frame := PanelContainer.new() map_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL @@ -163,13 +151,20 @@ func _build_ui() -> void: map_frame.add_theme_stylebox_override("panel", _panel_style(Color(0.065, 0.09, 0.09, 0.92), Color(0.19, 0.28, 0.27), 1, 0)) layout.add_child(map_frame) + var map_layer := Control.new() + map_layer.clip_contents = true + map_layer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + map_layer.size_flags_vertical = Control.SIZE_EXPAND_FILL + map_frame.add_child(map_layer) + grid_view = HexGridView.new() grid_view.clip_contents = true - grid_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL - grid_view.size_flags_vertical = Control.SIZE_EXPAND_FILL + grid_view.set_anchors_preset(Control.PRESET_FULL_RECT) grid_view.tile_hovered.connect(_on_tile_hovered) grid_view.tile_selected.connect(_on_tile_selected) - map_frame.add_child(grid_view) + map_layer.add_child(grid_view) + + _build_map_overlays(map_layer) var bottom_bar := PanelContainer.new() bottom_bar.custom_minimum_size = Vector2(0, 46) @@ -183,85 +178,56 @@ func _build_ui() -> void: bottom_margin.add_theme_constant_override("margin_bottom", 8) bottom_bar.add_child(bottom_margin) - var bottom_content := HBoxContainer.new() - bottom_content.add_theme_constant_override("separation", 16) - bottom_margin.add_child(bottom_content) + var status_row := HBoxContainer.new() + status_row.add_theme_constant_override("separation", 10) + bottom_margin.add_child(status_row) coordinate_label = Label.new() coordinate_label.text = "Drag to pan. Mouse wheel zooms. Select a tile to inspect territory." coordinate_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + coordinate_label.clip_text = true + coordinate_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS coordinate_label.add_theme_font_size_override("font_size", 14) coordinate_label.add_theme_color_override("font_color", Color(0.72, 0.8, 0.78)) - bottom_content.add_child(coordinate_label) + status_row.add_child(coordinate_label) - unit_label = Label.new() - unit_label.text = "Scouts: -- | Queue: --" - unit_label.custom_minimum_size = Vector2(142, 0) - unit_label.add_theme_font_size_override("font_size", 14) - unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) - bottom_content.add_child(unit_label) - - var training_box := VBoxContainer.new() - training_box.custom_minimum_size = Vector2(150, 0) - training_box.add_theme_constant_override("separation", 4) - bottom_content.add_child(training_box) - - training_label = Label.new() - training_label.text = "Training: idle" - training_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - training_label.add_theme_font_size_override("font_size", 12) - training_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) - training_box.add_child(training_label) - - training_bar = ProgressBar.new() - training_bar.custom_minimum_size = Vector2(150, 8) - training_bar.max_value = 1.0 - training_bar.value = 0.0 - training_bar.show_percentage = false - training_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26))) - training_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.28, 0.62, 0.72), Color(0.43, 0.82, 0.92))) - training_box.add_child(training_bar) - - var building_box := VBoxContainer.new() - building_box.custom_minimum_size = Vector2(150, 0) - building_box.add_theme_constant_override("separation", 4) - bottom_content.add_child(building_box) - - building_label = Label.new() - building_label.text = "Build: idle" - building_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - building_label.add_theme_font_size_override("font_size", 12) - building_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) - building_box.add_child(building_label) - - building_bar = ProgressBar.new() - building_bar.custom_minimum_size = Vector2(150, 8) - building_bar.max_value = 1.0 - building_bar.value = 0.0 - building_bar.show_percentage = false - building_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26))) - building_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.62, 0.42, 0.22), Color(0.86, 0.62, 0.34))) - building_box.add_child(building_bar) + action_buttons = HBoxContainer.new() + action_buttons.custom_minimum_size = Vector2(112, 0) + action_buttons.size_flags_horizontal = Control.SIZE_SHRINK_END + action_buttons.add_theme_constant_override("separation", 6) + status_row.add_child(action_buttons) train_button = _small_button("Train Scout 50g") train_button.visible = false train_button.pressed.connect(_train_scout) - bottom_content.add_child(train_button) + action_buttons.add_child(train_button) claim_button = _small_button("Claim 1 Scout") - claim_button.disabled = true + claim_button.visible = false claim_button.pressed.connect(_claim_selected_tile) - bottom_content.add_child(claim_button) + action_buttons.add_child(claim_button) build_log_camp_button = _small_button("Build Log Camp") - build_log_camp_button.disabled = true + build_log_camp_button.visible = false build_log_camp_button.pressed.connect(_build_log_camp) - bottom_content.add_child(build_log_camp_button) + action_buttons.add_child(build_log_camp_button) build_mine_button = _small_button("Build Mine") - build_mine_button.disabled = true + build_mine_button.visible = false build_mine_button.pressed.connect(_build_mine) - bottom_content.add_child(build_mine_button) + action_buttons.add_child(build_mine_button) + + build_barracks_button = _small_button("Build Barracks") + build_barracks_button.visible = false + build_barracks_button.pressed.connect(_build_barracks) + action_buttons.add_child(build_barracks_button) + + unit_label = Label.new() + unit_label.text = "Scouts: -- | Queue: --" + unit_label.custom_minimum_size = Vector2(126, 0) + unit_label.add_theme_font_size_override("font_size", 14) + unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) + status_row.add_child(unit_label) http_request = HTTPRequest.new() http_request.request_completed.connect(_on_home_request_completed) @@ -291,15 +257,144 @@ func _build_ui() -> void: build_request.request_completed.connect(_on_build_request_completed) add_child(build_request) + refresh_request = HTTPRequest.new() + refresh_request.request_completed.connect(_on_refresh_request_completed) + add_child(refresh_request) + + auth_check_timer = Timer.new() + auth_check_timer.wait_time = 15.0 + auth_check_timer.autostart = false + auth_check_timer.timeout.connect(_check_auth_expiry) + add_child(auth_check_timer) + resources_timer = Timer.new() resources_timer.wait_time = RESOURCE_REFRESH_SECONDS resources_timer.autostart = false resources_timer.timeout.connect(_refresh_player_state) add_child(resources_timer) + _build_refresh_dialog() _build_escape_menu() +func _build_map_overlays(map_layer: Control) -> void: + var resource_panel := PanelContainer.new() + resource_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + resource_panel.anchor_left = 1.0 + resource_panel.anchor_right = 1.0 + resource_panel.anchor_top = 0.0 + resource_panel.anchor_bottom = 0.0 + resource_panel.offset_left = -230 + resource_panel.offset_right = -16 + resource_panel.offset_top = 16 + resource_panel.offset_bottom = 128 + resource_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.045, 0.07, 0.07, 0.78), Color(0.18, 0.28, 0.26), 1, 4)) + map_layer.add_child(resource_panel) + + var resource_margin := MarginContainer.new() + resource_margin.add_theme_constant_override("margin_left", 12) + resource_margin.add_theme_constant_override("margin_right", 12) + resource_margin.add_theme_constant_override("margin_top", 10) + resource_margin.add_theme_constant_override("margin_bottom", 10) + resource_panel.add_child(resource_margin) + + var resource_stack := VBoxContainer.new() + resource_stack.add_theme_constant_override("separation", 6) + resource_margin.add_child(resource_stack) + + gold_label = Label.new() + gold_label.text = resource_text + gold_label.add_theme_font_size_override("font_size", 14) + gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32)) + resource_stack.add_child(gold_label) + + tick_label = Label.new() + tick_label.text = "Next: --" + tick_label.add_theme_font_size_override("font_size", 12) + tick_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) + resource_stack.add_child(tick_label) + + tick_bar = ProgressBar.new() + tick_bar.custom_minimum_size = Vector2(180, 8) + tick_bar.max_value = 1.0 + tick_bar.value = 0.0 + tick_bar.show_percentage = false + tick_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26))) + tick_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.82, 0.65, 0.2), Color(0.9, 0.78, 0.36))) + resource_stack.add_child(tick_bar) + + progress_panel = PanelContainer.new() + progress_panel.visible = false + progress_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + progress_panel.anchor_left = 0.0 + progress_panel.anchor_right = 0.0 + progress_panel.anchor_top = 0.0 + progress_panel.anchor_bottom = 0.0 + progress_panel.offset_left = 16 + progress_panel.offset_right = 220 + progress_panel.offset_top = 16 + progress_panel.offset_bottom = 130 + progress_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.045, 0.07, 0.07, 0.78), Color(0.18, 0.28, 0.26), 1, 4)) + map_layer.add_child(progress_panel) + + var progress_margin := MarginContainer.new() + progress_margin.add_theme_constant_override("margin_left", 12) + progress_margin.add_theme_constant_override("margin_right", 12) + progress_margin.add_theme_constant_override("margin_top", 10) + progress_margin.add_theme_constant_override("margin_bottom", 10) + progress_panel.add_child(progress_margin) + + var progress_stack := VBoxContainer.new() + progress_stack.add_theme_constant_override("separation", 8) + progress_margin.add_child(progress_stack) + + training_box = _progress_overlay_item("Training") + progress_stack.add_child(training_box) + training_label = training_box.get_child(0) as Label + training_bar = training_box.get_child(1) as ProgressBar + + building_box = _progress_overlay_item("Build") + progress_stack.add_child(building_box) + building_label = building_box.get_child(0) as Label + building_bar = building_box.get_child(1) as ProgressBar + building_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.62, 0.42, 0.22), Color(0.86, 0.62, 0.34))) + + +func _progress_overlay_item(label_text: String) -> VBoxContainer: + var box := VBoxContainer.new() + box.visible = false + box.custom_minimum_size = Vector2(180, 0) + box.add_theme_constant_override("separation", 4) + + var label := Label.new() + label.text = label_text + label.add_theme_font_size_override("font_size", 12) + label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) + box.add_child(label) + + var bar := ProgressBar.new() + bar.custom_minimum_size = Vector2(180, 8) + bar.max_value = 1.0 + bar.value = 0.0 + bar.show_percentage = false + bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26))) + bar.add_theme_stylebox_override("fill", _progress_style(Color(0.28, 0.62, 0.72), Color(0.43, 0.82, 0.92))) + box.add_child(bar) + + return box + + +func _build_refresh_dialog() -> void: + refresh_dialog = ConfirmationDialog.new() + refresh_dialog.title = "Session expiring" + refresh_dialog.dialog_text = "Your session is about to expire. Refresh it now?" + refresh_dialog.ok_button_text = "Refresh" + refresh_dialog.cancel_button_text = "Log Out" + refresh_dialog.confirmed.connect(_refresh_auth_token) + refresh_dialog.canceled.connect(_logout) + add_child(refresh_dialog) + + func _on_tile_hovered(q: int, r: int, terrain: String) -> void: if coordinate_label: coordinate_label.text = "Hex q:%d r:%d | %s" % [q, r, terrain] @@ -312,14 +407,8 @@ 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() - if train_button: - train_button.visible = selected_is_home - if claim_button: - 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 + selected_can_build_barracks = _can_build_barracks_on_selected_tile() + _update_action_buttons() if coordinate_label: coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain] @@ -332,6 +421,10 @@ func _can_build_mine_on_selected_tile() -> bool: return _can_build_on_selected_tile("Ore") +func _can_build_barracks_on_selected_tile() -> bool: + return _can_build_on_selected_tile("") + + func _can_build_on_selected_tile(required_terrain: String) -> bool: if not grid_view: return false @@ -343,13 +436,31 @@ func _can_build_on_selected_tile(required_terrain: String) -> bool: 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", "")) + var building_type: String = _nullable_string(location.get("buildingType", "")) - return owner_username == username and terrain == required_terrain and building_type.is_empty() + return owner_username == username and building_type.is_empty() and (required_terrain.is_empty() or terrain == required_terrain) func _can_claim_selected_tile() -> bool: - return selected_can_claim and completed_scout_count > 0 + return selected_can_claim and completed_scout_count > 0 and grid_view and grid_view.has_owned_neighbor(selected_q, selected_r) + + +func _update_action_buttons() -> void: + if train_button: + train_button.visible = selected_is_home + train_button.disabled = false + if claim_button: + claim_button.visible = _can_claim_selected_tile() + claim_button.disabled = false + if build_log_camp_button: + build_log_camp_button.visible = selected_can_build_log_camp + build_log_camp_button.disabled = false + if build_mine_button: + build_mine_button.visible = selected_can_build_mine + build_mine_button.disabled = false + if build_barracks_button: + build_barracks_button.visible = selected_can_build_barracks + build_barracks_button.disabled = false func _selected_tile_is_home() -> bool: @@ -391,6 +502,97 @@ func _load_session() -> Dictionary: return {} +func _start_auth_watch() -> void: + _check_auth_expiry() + if auth_check_timer: + auth_check_timer.start() + + +func _check_auth_expiry() -> void: + if session.is_empty() or refresh_prompt_active or refresh_request_active: + return + + var expires_unix := _parse_utc_timestamp(str(session.get("exp", ""))) + if expires_unix <= 0.0: + return + + var seconds_remaining := expires_unix - Time.get_unix_time_from_system() + if seconds_remaining <= 0.0: + _logout_deferred() + return + + if seconds_remaining <= TOKEN_REFRESH_PROMPT_SECONDS: + _prompt_refresh_or_logout("Your session expires in %d seconds. Refresh it now?" % int(ceil(seconds_remaining))) + + +func _prompt_refresh_or_logout(message: String) -> void: + if refresh_prompt_active or refresh_request_active: + return + + refresh_prompt_active = true + if refresh_dialog: + refresh_dialog.dialog_text = message + refresh_dialog.popup_centered() + + +func _refresh_auth_token() -> void: + refresh_prompt_active = false + var username := str(session.get("username", "")) + var refresh_token := str(session.get("refreshToken", "")) + if username.is_empty() or refresh_token.is_empty(): + _logout() + return + + refresh_request_active = true + var error: int = refresh_request.request( + AUTH_BASE_URL + "/api/auth/refresh", + PackedStringArray(["Content-Type: application/json"]), + HTTPClient.METHOD_POST, + JSON.stringify({ + "username": username, + "refreshToken": refresh_token + }) + ) + if error != OK: + refresh_request_active = false + _logout() + + +func _on_refresh_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + refresh_request_active = false + if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: + _logout() + return + + var parsed = JSON.parse_string(body.get_string_from_utf8()) + if typeof(parsed) != TYPE_DICTIONARY: + _logout() + return + + var refreshed: Dictionary = parsed + session["accessToken"] = refreshed.get("accessToken", session.get("accessToken", "")) + session["exp"] = refreshed.get("exp", session.get("exp", "")) + get_tree().set_meta("auth_session", session) + _save_session_if_present(session) + coordinate_label.text = "Session refreshed." + _check_auth_expiry() + + +func _save_session(next_session: Dictionary) -> void: + var file := FileAccess.open(SESSION_PATH, FileAccess.WRITE) + if file: + file.store_string(JSON.stringify(next_session)) + + +func _save_session_if_present(next_session: Dictionary) -> void: + if FileAccess.file_exists(SESSION_PATH): + _save_session(next_session) + + +func _logout_deferred() -> void: + call_deferred("_logout") + + func _assign_home_location() -> void: var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): @@ -484,7 +686,8 @@ func _refresh_player_state() -> void: func _load_resources() -> void: var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): - gold_label.text = "Gold: --" + resource_text = "Gold: --\nWood: --\nOre: --" + gold_label.text = resource_text return if resources_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: @@ -498,17 +701,20 @@ func _load_resources() -> void: HTTPClient.METHOD_GET ) if error != OK: - gold_label.text = "Gold: ?" + coordinate_label.text = "Resource request could not start. Error: %d" % error func _on_resources_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: - gold_label.text = "Gold: ?" + if response_code == 401: + _prompt_refresh_or_logout("Your session expired. Refresh it now?") + return + coordinate_label.text = "Resource request failed. result=%d status=%d body=%s" % [result, response_code, body.get_string_from_utf8()] return var parsed = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: - gold_label.text = "Gold: ?" + coordinate_label.text = "Resource service returned invalid JSON." return var resources: Dictionary = parsed @@ -518,7 +724,8 @@ func _on_resources_request_completed(result: int, response_code: int, _headers: var wood_per_minute: int = int(resources.get("woodPerMinute", 0)) 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] + resource_text = "Gold: %d (+%d/min)\nWood: %d (+%d/min)\nOre: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute, ore, ore_per_minute] + gold_label.text = resource_text var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", "")) gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text) @@ -565,8 +772,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack 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() + _update_action_buttons() if training.is_empty(): training_started_unix = 0.0 @@ -640,6 +846,9 @@ func _claim_selected_tile() -> void: if completed_scout_count <= 0: coordinate_label.text = "Train a scout before claiming territory." return + if not grid_view.has_owned_neighbor(selected_q, selected_r): + coordinate_label.text = "Claims must be adjacent to controlled territory." + return var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): @@ -661,7 +870,7 @@ func _claim_selected_tile() -> void: JSON.stringify({ "q": selected_q, "r": selected_r }) ) if error != OK: - claim_button.disabled = false + _update_action_buttons() coordinate_label.text = "Could not contact the locations service." @@ -669,17 +878,17 @@ func _on_claim_request_completed(result: int, response_code: int, _headers: Pack var text: String = body.get_string_from_utf8() if result != HTTPRequest.RESULT_SUCCESS: coordinate_label.text = "Scout claim request failed." - claim_button.disabled = not selected_can_claim + _update_action_buttons() return if response_code < 200 or response_code >= 300: coordinate_label.text = "Scout claim failed: %s" % text - claim_button.disabled = not selected_can_claim + _update_action_buttons() _refresh_player_state() return selected_can_claim = false - claim_button.disabled = true + _update_action_buttons() coordinate_label.text = "Scout claimed q:%d r:%d. Territory expanded." % [selected_q, selected_r] _load_visible_locations() _refresh_player_state() @@ -701,6 +910,14 @@ func _build_mine() -> void: _request_building("Mine") +func _build_barracks() -> void: + if not selected_can_build_barracks: + coordinate_label.text = "Select controlled territory without a building." + return + + _request_building("Barracks") + + func _request_building(building_type: String) -> void: var access_token: String = str(session.get("accessToken", "")) if access_token.is_empty(): @@ -710,10 +927,13 @@ func _request_building(building_type: String) -> void: if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return - if building_type == "Mine": - build_mine_button.disabled = true - else: - build_log_camp_button.disabled = true + match building_type: + "Mine": + build_mine_button.disabled = true + "Barracks": + build_barracks_button.disabled = true + _: + 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( @@ -726,8 +946,7 @@ func _request_building(building_type: String) -> void: JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": building_type }) ) if error != OK: - build_log_camp_button.disabled = not selected_can_build_log_camp - build_mine_button.disabled = not selected_can_build_mine + _update_action_buttons() coordinate_label.text = "Could not contact the locations service." @@ -735,25 +954,23 @@ func _on_build_request_completed(result: int, response_code: int, _headers: Pack var text: String = body.get_string_from_utf8() if result != HTTPRequest.RESULT_SUCCESS: 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 + _update_action_buttons() return if response_code < 200 or response_code >= 300: 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 + _update_action_buttons() _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 + selected_can_build_barracks = false + _update_action_buttons() 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"))) + active_building_label = _building_display_name(_nullable_string(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 @@ -787,10 +1004,13 @@ func _update_training_bar() -> void: return if training_started_unix <= 0.0 or training_completes_unix <= 0.0: + training_box.visible = false training_bar.value = 0.0 training_label.text = "Training: idle" + _update_progress_panel_visibility() return + training_box.visible = true var now: float = Time.get_unix_time_from_system() var duration: float = max(1.0, training_completes_unix - training_started_unix) var elapsed: float = clamp(now - training_started_unix, 0.0, duration) @@ -804,6 +1024,7 @@ func _update_training_bar() -> void: _load_units() else: training_label.text = "Training: %ds" % remaining + _update_progress_panel_visibility() func _update_building_timer() -> void: @@ -811,10 +1032,13 @@ func _update_building_timer() -> void: return if building_started_unix <= 0.0 or building_completes_unix <= 0.0: + building_box.visible = false building_bar.value = 0.0 building_label.text = "Build: idle" + _update_progress_panel_visibility() return + building_box.visible = true var now: float = Time.get_unix_time_from_system() var duration: float = max(1.0, building_completes_unix - building_started_unix) var elapsed: float = clamp(now - building_started_unix, 0.0, duration) @@ -830,6 +1054,12 @@ func _update_building_timer() -> void: building_completion_refresh_requested = true _load_visible_locations() _refresh_player_state() + _update_progress_panel_visibility() + + +func _update_progress_panel_visibility() -> void: + if progress_panel: + progress_panel.visible = (training_box and training_box.visible) or (building_box and building_box.visible) func _sync_active_building_timer(locations: Array, username: String) -> void: @@ -844,7 +1074,7 @@ func _sync_active_building_timer(locations: Array, username: String) -> void: var location: Dictionary = item if str(location.get("ownerUsername", "")) != username: continue - var building_type: String = str(location.get("buildingType", "")) + var building_type: String = _nullable_string(location.get("buildingType", "")) if building_type.is_empty(): continue if str(location.get("buildingStatus", "")) != "Building": @@ -872,6 +1102,8 @@ func _building_display_name(building_type: String) -> String: return "Log Camp" "Mine": return "Mine" + "Barracks": + return "Barracks" _: return "Build" @@ -895,6 +1127,12 @@ func _parse_utc_timestamp(timestamp: String) -> float: return Time.get_unix_time_from_datetime_string(normalized) +func _nullable_string(value: Variant) -> String: + if value == null: + return "" + return str(value) + + func _build_escape_menu() -> void: escape_overlay = Control.new() escape_overlay.visible = false @@ -995,8 +1233,10 @@ func _menu_button(text_value: String) -> Button: func _small_button(text_value: String) -> Button: var button := _menu_button(text_value) - button.custom_minimum_size = Vector2(128, 32) - button.add_theme_font_size_override("font_size", 13) + button.custom_minimum_size = Vector2(108, 32) + button.add_theme_font_size_override("font_size", 11) + button.clip_text = true + button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS return button diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index 8e5815f..a4c12f5 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -161,14 +161,16 @@ func _draw_tile(tile: Dictionary) -> void: draw_polyline(_closed_points(points), Color(1.0, 0.88, 0.38), 3.0) 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", "")) + var building_type: String = _nullable_string(visible_location.get("buildingType", "")) + var building_status: String = _nullable_string(visible_location.get("buildingStatus", "")) 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 == "Mine" and (building_status == "Built" or building_status.is_empty()): _draw_mine_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(): _draw_building_marker(center, height) @@ -329,6 +331,37 @@ 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_barracks_marker(center: Vector2, height: float) -> void: + var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0) + var width: float = 22.0 * zoom + var marker_height: float = 15.0 * zoom + var wall_color := Color(0.52, 0.36, 0.24) + var roof_color := Color(0.25, 0.18, 0.13) + var banner_color := Color(0.82, 0.18, 0.16) + var trim_color := Color(0.78, 0.58, 0.34) + var shadow_color := Color(0.09, 0.06, 0.04) + + var body_rect := Rect2(marker_center + Vector2(-width * 0.42, -marker_height * 0.05), Vector2(width * 0.84, marker_height * 0.62)) + var roof := PackedVector2Array([ + marker_center + Vector2(-width * 0.52, -marker_height * 0.05), + marker_center + Vector2(0, -marker_height * 0.58), + marker_center + Vector2(width * 0.52, -marker_height * 0.05) + ]) + var banner := PackedVector2Array([ + marker_center + Vector2(width * 0.16, -marker_height * 0.7), + marker_center + Vector2(width * 0.42, -marker_height * 0.62), + marker_center + Vector2(width * 0.16, -marker_height * 0.46) + ]) + + 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_line(marker_center + Vector2(-width * 0.52, marker_height * 0.58), marker_center + Vector2(width * 0.52, marker_height * 0.58), trim_color, 2.0 * zoom) + draw_line(marker_center + Vector2(width * 0.16, -marker_height * 0.78), marker_center + Vector2(width * 0.16, marker_height * 0.1), trim_color.darkened(0.25), 2.0 * zoom) + draw_colored_polygon(banner, banner_color) + draw_rect(Rect2(marker_center + Vector2(-width * 0.1, marker_height * 0.24), Vector2(width * 0.2, marker_height * 0.33)), shadow_color.lightened(0.2), true) + + 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 @@ -434,6 +467,13 @@ func get_visible_location(q: int, r: int) -> Dictionary: return {} +func has_owned_neighbor(q: int, r: int) -> bool: + for neighbor in _neighbor_coordinates(q, r): + if owned_tiles.has(_tile_key(neighbor.x, neighbor.y)): + return true + return false + + func _tile_key(q: int, r: int) -> String: return "%d,%d" % [q, r] @@ -481,13 +521,15 @@ func _neighbor_coordinates(q: int, r: int) -> Array[Vector2i]: func _tile_display_label(key: String, data: Dictionary) -> String: var terrain: String = str(data.get("terrain", "Open")) - var building_type: String = str(data.get("buildingType", "")) - var building_status: String = str(data.get("buildingStatus", "")) + var building_type: String = _nullable_string(data.get("buildingType", "")) + var building_status: String = _nullable_string(data.get("buildingStatus", "")) 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" + elif building_type == "Barracks": + building_label = " | Barracks" if building_status == "Built" or building_status.is_empty() else " | Barracks building" if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))): return "%s | Your home%s" % [terrain, building_label] @@ -509,6 +551,12 @@ func _owner_username(location: Dictionary) -> String: return str(owner_value) +func _nullable_string(value: Variant) -> String: + if value == null: + return "" + return str(value) + + func _tile_height(q: int, r: int) -> float: var distance: int = max(max(abs(q), abs(r)), abs(-q - r)) if distance <= 1: diff --git a/microservices/LocationsApi/Controllers/LocationsController.cs b/microservices/LocationsApi/Controllers/LocationsController.cs index 2983ba5..cf2ca22 100644 --- a/microservices/LocationsApi/Controllers/LocationsController.cs +++ b/microservices/LocationsApi/Controllers/LocationsController.cs @@ -88,6 +88,11 @@ public class LocationsController : ControllerBase return NotFound(); } + if (!User.IsInRole("SUPER") && !await _locations.IsAdjacentToOwnedAsync(username, request.Q, request.R)) + { + return BadRequest("Territory claims must be adjacent to controlled territory."); + } + var existingLocation = await _locations.GetAsync(request.Q, request.R); if (existingLocation is not null && !string.IsNullOrWhiteSpace(existingLocation.OwnerUsername)) { diff --git a/microservices/LocationsApi/Models/Dto.cs b/microservices/LocationsApi/Models/Dto.cs index de1d016..c306a5d 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" || location.BuildingType == "Mine") && string.IsNullOrWhiteSpace(location.BuildingStatus) + BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(location.BuildingStatus) ? "Built" : location.BuildingStatus, BuildingStartedAtUtc = location.BuildingStartedAtUtc, diff --git a/microservices/LocationsApi/README.md b/microservices/LocationsApi/README.md index acbd0ce..8cf193d 100644 --- a/microservices/LocationsApi/README.md +++ b/microservices/LocationsApi/README.md @@ -44,8 +44,10 @@ training record. 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. +build, and adds 5 ore per minute after it finishes. `Barracks` can be built on +any controlled territory, costs 500 gold, 100 wood, and 100 ore, and takes 60 +seconds to build. 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: @@ -53,3 +55,4 @@ seeded on first read: - `ScoutTrainingSeconds` - `LogCampBuildSeconds` - `MineBuildSeconds` +- `BarracksBuildSeconds` diff --git a/microservices/LocationsApi/Services/GameSettingsService.cs b/microservices/LocationsApi/Services/GameSettingsService.cs index 1cce4c1..2ba5e24 100644 --- a/microservices/LocationsApi/Services/GameSettingsService.cs +++ b/microservices/LocationsApi/Services/GameSettingsService.cs @@ -8,6 +8,7 @@ public class GameSettingsService public const string ScoutTrainingSeconds = "ScoutTrainingSeconds"; public const string LogCampBuildSeconds = "LogCampBuildSeconds"; public const string MineBuildSeconds = "MineBuildSeconds"; + public const string BarracksBuildSeconds = "BarracksBuildSeconds"; private readonly IMongoCollection _settings; diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index 8981174..5650252 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -15,6 +15,10 @@ public class LocationService private readonly int _mineOrePerMinute; private readonly int _defaultLogCampBuildSeconds; private readonly int _defaultMineBuildSeconds; + private readonly int _defaultBarracksBuildSeconds; + private readonly int _barracksGoldCost; + private readonly int _barracksWoodCost; + private readonly int _barracksOreCost; public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings) { @@ -40,6 +44,18 @@ public class LocationService _defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds) ? Math.Max(1, mineBuildSeconds) : 60; + _defaultBarracksBuildSeconds = int.TryParse(cfg["Buildings:BarracksBuildSeconds"], out var barracksBuildSeconds) + ? Math.Max(1, barracksBuildSeconds) + : 60; + _barracksGoldCost = int.TryParse(cfg["Buildings:BarracksGoldCost"], out var barracksGoldCost) + ? Math.Max(0, barracksGoldCost) + : 500; + _barracksWoodCost = int.TryParse(cfg["Buildings:BarracksWoodCost"], out var barracksWoodCost) + ? Math.Max(0, barracksWoodCost) + : 100; + _barracksOreCost = int.TryParse(cfg["Buildings:BarracksOreCost"], out var barracksOreCost) + ? Math.Max(0, barracksOreCost) + : 100; var coordinateIndex = Builders.IndexKeys .Ascending(x => x.Q) @@ -149,8 +165,8 @@ public class LocationService throw new InvalidOperationException("This location already has a building or construction in progress."); } - var requiredTerrain = buildingType == "LogCamp" ? "Forest" : "Ore"; - if (!string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase)) + var requiredTerrain = RequiredTerrainForBuilding(buildingType); + if (requiredTerrain is not null && !string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory."); } @@ -158,12 +174,8 @@ public class LocationService await CompleteFinishedBuildingsAsync(username); var now = DateTime.UtcNow; - var settingKey = buildingType == "LogCamp" - ? GameSettingsService.LogCampBuildSeconds - : GameSettingsService.MineBuildSeconds; - var defaultBuildSeconds = buildingType == "LogCamp" - ? _defaultLogCampBuildSeconds - : _defaultMineBuildSeconds; + var settingKey = BuildSecondsSettingKey(buildingType); + var defaultBuildSeconds = DefaultBuildSeconds(buildingType); var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds); var update = Builders.Update .Set(x => x.BuildingType, buildingType) @@ -173,7 +185,7 @@ public class LocationService .Set(x => x.UpdatedAtUtc, now); var updated = await _locations.FindOneAndUpdateAsync( - x => x.Id == location.Id && x.OwnerUsername == username && x.BuildingType == null, + x => x.Id == location.Id && x.OwnerUsername == username && (x.BuildingType == null || x.BuildingType == ""), update, new FindOneAndUpdateOptions { @@ -185,6 +197,27 @@ public class LocationService throw new InvalidOperationException("This location already has a building or construction in progress."); } + if (buildingType == "Barracks") + { + var spent = await _resources.TrySpendResourcesAsync(username, _barracksGoldCost, _barracksWoodCost, _barracksOreCost); + if (!spent) + { + var rollback = Builders.Update + .Unset(x => x.BuildingType) + .Unset(x => x.BuildingStatus) + .Unset(x => x.BuildingStartedAtUtc) + .Unset(x => x.BuildingCompletesAtUtc) + .Unset(x => x.BuildingBuiltAtUtc) + .Set(x => x.UpdatedAtUtc, DateTime.UtcNow); + + await _locations.UpdateOneAsync( + 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."); + } + } + return updated; } @@ -193,7 +226,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 == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Barracks") && x.BuildingStatus == "Building" && x.BuildingCompletesAtUtc <= now) .ToListAsync(); @@ -353,6 +386,15 @@ public class LocationService return ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth); } + public async Task IsAdjacentToOwnedAsync(string username, int q, int r) + { + var neighbors = NeighborCoordinates(q, r).ToList(); + return await _locations.Find(x => + x.OwnerUsername == username && + neighbors.Any(n => n.Q == x.Q && n.R == x.R)) + .AnyAsync(); + } + private async Task TryCreateHomeAsync(string username, int q, int r) { var now = DateTime.UtcNow; @@ -582,7 +624,35 @@ public class LocationService { "logcamp" or "log camp" => "LogCamp", "mine" => "Mine", + "barracks" => "Barracks", _ => null }; } + + private static string? RequiredTerrainForBuilding(string buildingType) => + buildingType switch + { + "LogCamp" => "Forest", + "Mine" => "Ore", + "Barracks" => null, + _ => null + }; + + private static string BuildSecondsSettingKey(string buildingType) => + buildingType switch + { + "LogCamp" => GameSettingsService.LogCampBuildSeconds, + "Mine" => GameSettingsService.MineBuildSeconds, + "Barracks" => GameSettingsService.BarracksBuildSeconds, + _ => GameSettingsService.LogCampBuildSeconds + }; + + private int DefaultBuildSeconds(string buildingType) => + buildingType switch + { + "LogCamp" => _defaultLogCampBuildSeconds, + "Mine" => _defaultMineBuildSeconds, + "Barracks" => _defaultBarracksBuildSeconds, + _ => 60 + }; } diff --git a/microservices/LocationsApi/Services/ResourceService.cs b/microservices/LocationsApi/Services/ResourceService.cs index 0eafd6a..b38f572 100644 --- a/microservices/LocationsApi/Services/ResourceService.cs +++ b/microservices/LocationsApi/Services/ResourceService.cs @@ -82,6 +82,35 @@ public class ResourceService return updated is not null; } + public async Task TrySpendResourcesAsync(string username, int gold, int wood, int ore) + { + gold = Math.Max(0, gold); + wood = Math.Max(0, wood); + ore = Math.Max(0, ore); + if (gold == 0 && wood == 0 && ore == 0) + { + return true; + } + + await EnsureAsync(username); + + var update = Builders.Update + .Inc(x => x.Gold, -gold) + .Inc(x => x.Wood, -wood) + .Inc(x => x.Ore, -ore) + .Set(x => x.UpdatedAtUtc, DateTime.UtcNow); + + var updated = await _resources.FindOneAndUpdateAsync( + x => x.Username == username && x.Gold >= gold && x.Wood >= wood && x.Ore >= ore, + update, + new FindOneAndUpdateOptions + { + ReturnDocument = ReturnDocument.After + }); + + return updated is not null; + } + public async Task AddWoodGenerationAsync(string username, int woodPerMinute) { if (woodPerMinute <= 0) diff --git a/microservices/LocationsApi/appsettings.json b/microservices/LocationsApi/appsettings.json index 559e4f4..da8174d 100644 --- a/microservices/LocationsApi/appsettings.json +++ b/microservices/LocationsApi/appsettings.json @@ -31,7 +31,11 @@ "LogCampWoodPerMinute": 5, "LogCampBuildSeconds": 60, "MineOrePerMinute": 5, - "MineBuildSeconds": 60 + "MineBuildSeconds": 60, + "BarracksBuildSeconds": 60, + "BarracksGoldCost": 500, + "BarracksWoodCost": 100, + "BarracksOreCost": 100 }, "AllowedHosts": "*" } diff --git a/microservices/LocationsApi/k8s/configmap.yaml b/microservices/LocationsApi/k8s/configmap.yaml index ffff5c6..14dcbac 100644 --- a/microservices/LocationsApi/k8s/configmap.yaml +++ b/microservices/LocationsApi/k8s/configmap.yaml @@ -19,3 +19,7 @@ data: Buildings__LogCampBuildSeconds: "60" Buildings__MineOrePerMinute: "5" Buildings__MineBuildSeconds: "60" + Buildings__BarracksBuildSeconds: "60" + Buildings__BarracksGoldCost: "500" + Buildings__BarracksWoodCost: "100" + Buildings__BarracksOreCost: "100"