diff --git a/game/territory-autobattler/scenes/game/hex_grid_screen.gd b/game/territory-autobattler/scenes/game/hex_grid_screen.gd index 41bb9fa..27eff06 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_screen.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_screen.gd @@ -28,6 +28,8 @@ var train_button: Button var unit_label: Label var training_label: Label var training_bar: ProgressBar +var building_label: Label +var building_bar: ProgressBar var training_started_unix := 0.0 var training_completes_unix := 0.0 var training_completion_refresh_requested := false @@ -215,6 +217,27 @@ func _build_ui() -> void: 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) + train_button = _small_button("Train Scout 50g") train_button.pressed.connect(_train_scout) bottom_content.add_child(train_button) @@ -388,6 +411,7 @@ func _on_visible_request_completed(result: int, response_code: int, _headers: Pa var username: String = str(session.get("username", "")) var visible_locations: Array = parsed grid_view.set_visible_locations(visible_locations, username) + _sync_active_building_timer(visible_locations, username) coordinate_label.text = "Visible territory loaded. Sight extends 3 hexes beyond controlled land." _refresh_player_state() resources_timer.start() @@ -644,6 +668,7 @@ func _on_build_request_completed(result: int, response_code: int, _headers: Pack building_started_unix = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", ""))) building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", ""))) building_completion_refresh_requested = false + _update_building_timer() coordinate_label.text = "Log Camp construction started. Completion takes 1 minute." _load_visible_locations() _refresh_player_state() @@ -693,19 +718,61 @@ func _update_training_bar() -> void: func _update_building_timer() -> void: + if not building_bar or not building_label: + return + if building_started_unix <= 0.0 or building_completes_unix <= 0.0: + building_bar.value = 0.0 + building_label.text = "Build: idle" return - var remaining: int = int(max(0, int(ceil(building_completes_unix - Time.get_unix_time_from_system())))) + 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) + var remaining: int = int(max(0, int(ceil(building_completes_unix - now)))) + building_bar.value = elapsed / duration + if remaining > 0: - return + building_label.text = "Log Camp: %ds" % remaining + else: + building_label.text = "Log Camp: ready" - if not building_completion_refresh_requested: + if remaining <= 0 and not building_completion_refresh_requested: building_completion_refresh_requested = true _load_visible_locations() _refresh_player_state() +func _sync_active_building_timer(locations: Array, username: String) -> void: + var best_started := 0.0 + var best_completes := 0.0 + + for item in locations: + if typeof(item) != TYPE_DICTIONARY: + continue + + var location: Dictionary = item + if str(location.get("ownerUsername", "")) != username: + continue + if str(location.get("buildingType", "")) != "LogCamp": + continue + if str(location.get("buildingStatus", "")) != "Building": + continue + + var started: float = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", ""))) + var completes: float = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", ""))) + if completes <= 0.0: + continue + if best_completes <= 0.0 or completes < best_completes: + best_started = started + best_completes = completes + + building_started_unix = best_started + building_completes_unix = best_completes + building_completion_refresh_requested = false + _update_building_timer() + + func _parse_utc_timestamp(timestamp: String) -> float: if timestamp.is_empty(): return 0.0 diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index 6a4d133..e38cf23 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -303,9 +303,26 @@ func _draw_log_camp_marker(center: Vector2, height: float) -> void: func _draw_building_marker(center: Vector2, height: float) -> void: var marker_center: Vector2 = center + Vector2(0, -height * zoom - 1.0) - var radius: float = 7.0 * zoom - draw_arc(marker_center, radius, 0.0, TAU, 24, Color(0.95, 0.76, 0.28), 2.0 * zoom) - draw_line(marker_center + Vector2(-radius * 0.7, radius * 0.7), marker_center + Vector2(radius * 0.7, -radius * 0.7), Color(0.95, 0.76, 0.28), 2.0 * zoom) + var width: float = 20.0 * zoom + var scaffold_height: float = 15.0 * zoom + var post_color := Color(0.96, 0.68, 0.22) + var shadow_color := Color(0.18, 0.12, 0.06) + var left: Vector2 = marker_center + Vector2(-width * 0.45, scaffold_height * 0.45) + var right: Vector2 = marker_center + Vector2(width * 0.45, scaffold_height * 0.45) + var top_left: Vector2 = marker_center + Vector2(-width * 0.32, -scaffold_height * 0.42) + var top_right: Vector2 = marker_center + Vector2(width * 0.32, -scaffold_height * 0.42) + var peak: Vector2 = marker_center + Vector2(0, -scaffold_height * 0.78) + + draw_line(left + Vector2(1, 1) * zoom, top_left + Vector2(1, 1) * zoom, shadow_color, 3.0 * zoom) + draw_line(right + Vector2(1, 1) * zoom, top_right + Vector2(1, 1) * zoom, shadow_color, 3.0 * zoom) + draw_line(top_left + Vector2(1, 1) * zoom, top_right + Vector2(1, 1) * zoom, shadow_color, 3.0 * zoom) + draw_line(left, top_left, post_color, 2.0 * zoom) + draw_line(right, top_right, post_color, 2.0 * zoom) + draw_line(top_left, peak, post_color.lightened(0.08), 2.0 * zoom) + draw_line(peak, top_right, post_color.lightened(0.08), 2.0 * zoom) + draw_line(top_left, top_right, post_color, 2.0 * zoom) + draw_line(left.lerp(top_left, 0.52), right.lerp(top_right, 0.52), post_color.darkened(0.08), 2.0 * zoom) + draw_line(left, right.lerp(top_right, 0.2), post_color.darkened(0.12), 1.5 * zoom) func _tile_data(q: int, r: int) -> Dictionary: diff --git a/microservices/LocationsApi/README.md b/microservices/LocationsApi/README.md index 815ffb7..8a464c4 100644 --- a/microservices/LocationsApi/README.md +++ b/microservices/LocationsApi/README.md @@ -43,7 +43,8 @@ unclaimed visible location consumes one completed Scout. Buildings are stored on location records. The first building type is `LogCamp`, which can be built on controlled Forest territory, takes 60 seconds to build, and adds 5 wood per minute to the owning player's resource generation after it -finishes. +finishes. Each location has one building slot; a building under construction +occupies that slot. Timer settings are stored in the MongoDB `GameSettings` collection and are seeded on first read: diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index 5e42084..545df59 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -137,7 +137,7 @@ public class LocationService if (!string.IsNullOrWhiteSpace(location.BuildingType)) { - throw new InvalidOperationException("This location already has a building."); + throw new InvalidOperationException("This location already has a building or construction in progress."); } if (!string.Equals(location.Terrain, "Forest", StringComparison.OrdinalIgnoreCase)) @@ -166,7 +166,7 @@ public class LocationService if (updated is null) { - throw new InvalidOperationException("This location already has a building."); + throw new InvalidOperationException("This location already has a building or construction in progress."); } return updated;