Adding barracks

This commit is contained in:
hz
2026-05-19 04:42:57 -05:00
parent 34d406e993
commit 78513603ab
10 changed files with 556 additions and 152 deletions
@@ -2,7 +2,9 @@ extends Control
const SESSION_PATH := "user://auth_session.json" const SESSION_PATH := "user://auth_session.json"
const LOCATIONS_BASE_URL := "https://tloc.ranaze.com" const LOCATIONS_BASE_URL := "https://tloc.ranaze.com"
const AUTH_BASE_URL := "https://tauth.ranaze.com"
const RESOURCE_REFRESH_SECONDS := 15.0 const RESOURCE_REFRESH_SECONDS := 15.0
const TOKEN_REFRESH_PROMPT_SECONDS := 120.0
const HexGridView := preload("res://scenes/game/hex_grid_view.gd") const HexGridView := preload("res://scenes/game/hex_grid_view.gd")
var coordinate_label: Label var coordinate_label: Label
@@ -15,21 +17,29 @@ var units_request: HTTPRequest
var train_unit_request: HTTPRequest var train_unit_request: HTTPRequest
var claim_request: HTTPRequest var claim_request: HTTPRequest
var build_request: HTTPRequest var build_request: HTTPRequest
var refresh_request: HTTPRequest
var resources_timer: Timer var resources_timer: Timer
var auth_check_timer: Timer
var refresh_dialog: ConfirmationDialog
var session: Dictionary = {} var session: Dictionary = {}
var escape_overlay: Control var escape_overlay: Control
var escape_panel: PanelContainer var escape_panel: PanelContainer
var gold_label: Label var gold_label: Label
var tick_bar: ProgressBar var tick_bar: ProgressBar
var tick_label: Label var tick_label: Label
var resource_text := "Gold: --\nWood: --\nOre: --"
var gold_tick_anchor_unix := 0.0 var gold_tick_anchor_unix := 0.0
var gold_tick_seconds := 60.0 var gold_tick_seconds := 60.0
var train_button: Button var train_button: Button
var unit_label: Label var unit_label: Label
var training_box: VBoxContainer
var training_label: Label var training_label: Label
var training_bar: ProgressBar var training_bar: ProgressBar
var building_box: VBoxContainer
var building_label: Label var building_label: Label
var building_bar: ProgressBar var building_bar: ProgressBar
var progress_panel: PanelContainer
var action_buttons: HBoxContainer
var training_started_unix := 0.0 var training_started_unix := 0.0
var training_completes_unix := 0.0 var training_completes_unix := 0.0
var training_completion_refresh_requested := false var training_completion_refresh_requested := false
@@ -40,19 +50,24 @@ var active_building_label := "Build"
var claim_button: Button var claim_button: Button
var build_log_camp_button: Button var build_log_camp_button: Button
var build_mine_button: Button var build_mine_button: Button
var build_barracks_button: Button
var selected_q := 0 var selected_q := 0
var selected_r := 0 var selected_r := 0
var selected_can_claim := false var selected_can_claim := false
var selected_can_build_log_camp := false var selected_can_build_log_camp := false
var selected_can_build_mine := false var selected_can_build_mine := false
var selected_can_build_barracks := false
var selected_is_home := false var selected_is_home := false
var completed_scout_count := 0 var completed_scout_count := 0
var refresh_prompt_active := false
var refresh_request_active := false
func _ready() -> void: func _ready() -> void:
session = _load_session() session = _load_session()
_build_ui() _build_ui()
_assign_home_location() _assign_home_location()
_start_auth_watch()
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
@@ -100,10 +115,10 @@ func _build_ui() -> void:
top_margin.add_theme_constant_override("margin_bottom", 12) top_margin.add_theme_constant_override("margin_bottom", 12)
top_bar.add_child(top_margin) top_bar.add_child(top_margin)
var top_content := HBoxContainer.new() var title_row := HBoxContainer.new()
top_content.alignment = BoxContainer.ALIGNMENT_CENTER title_row.alignment = BoxContainer.ALIGNMENT_CENTER
top_content.add_theme_constant_override("separation", 16) title_row.add_theme_constant_override("separation", 16)
top_margin.add_child(top_content) top_margin.add_child(title_row)
var title := Label.new() var title := Label.new()
title.text = "Territory Map" 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)) title.add_theme_color_override("font_color", Color(0.94, 0.96, 0.9))
if title_font: if title_font:
title.add_theme_font_override("font", title_font) title.add_theme_font_override("font", title_font)
top_content.add_child(title) title_row.add_child(title)
var legend := HBoxContainer.new() var legend := HBoxContainer.new()
legend.add_theme_constant_override("separation", 12) 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(1.0, 0.76, 0.16), "Home"))
legend.add_child(_legend_item(Color(0.16, 0.86, 0.92), "Yours")) 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.38, 0.56, 0.36), "Open"))
legend.add_child(_legend_item(Color(0.045, 0.065, 0.065), "Unknown")) 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() var user_label := Label.new()
user_label.text = _session_name() user_label.text = _session_name()
user_label.add_theme_font_size_override("font_size", 15) 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)) 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() var map_frame := PanelContainer.new()
map_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL 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)) 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) 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 = HexGridView.new()
grid_view.clip_contents = true grid_view.clip_contents = true
grid_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL grid_view.set_anchors_preset(Control.PRESET_FULL_RECT)
grid_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
grid_view.tile_hovered.connect(_on_tile_hovered) grid_view.tile_hovered.connect(_on_tile_hovered)
grid_view.tile_selected.connect(_on_tile_selected) 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() var bottom_bar := PanelContainer.new()
bottom_bar.custom_minimum_size = Vector2(0, 46) 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_margin.add_theme_constant_override("margin_bottom", 8)
bottom_bar.add_child(bottom_margin) bottom_bar.add_child(bottom_margin)
var bottom_content := HBoxContainer.new() var status_row := HBoxContainer.new()
bottom_content.add_theme_constant_override("separation", 16) status_row.add_theme_constant_override("separation", 10)
bottom_margin.add_child(bottom_content) bottom_margin.add_child(status_row)
coordinate_label = Label.new() coordinate_label = Label.new()
coordinate_label.text = "Drag to pan. Mouse wheel zooms. Select a tile to inspect territory." 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.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_font_size_override("font_size", 14)
coordinate_label.add_theme_color_override("font_color", Color(0.72, 0.8, 0.78)) 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() action_buttons = HBoxContainer.new()
unit_label.text = "Scouts: -- | Queue: --" action_buttons.custom_minimum_size = Vector2(112, 0)
unit_label.custom_minimum_size = Vector2(142, 0) action_buttons.size_flags_horizontal = Control.SIZE_SHRINK_END
unit_label.add_theme_font_size_override("font_size", 14) action_buttons.add_theme_constant_override("separation", 6)
unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78)) status_row.add_child(action_buttons)
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)
train_button = _small_button("Train Scout 50g") train_button = _small_button("Train Scout 50g")
train_button.visible = false train_button.visible = false
train_button.pressed.connect(_train_scout) 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 = _small_button("Claim 1 Scout")
claim_button.disabled = true claim_button.visible = false
claim_button.pressed.connect(_claim_selected_tile) 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 = _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) 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 = _small_button("Build Mine")
build_mine_button.disabled = true build_mine_button.visible = false
build_mine_button.pressed.connect(_build_mine) 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 = HTTPRequest.new()
http_request.request_completed.connect(_on_home_request_completed) 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) build_request.request_completed.connect(_on_build_request_completed)
add_child(build_request) 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 = Timer.new()
resources_timer.wait_time = RESOURCE_REFRESH_SECONDS resources_timer.wait_time = RESOURCE_REFRESH_SECONDS
resources_timer.autostart = false resources_timer.autostart = false
resources_timer.timeout.connect(_refresh_player_state) resources_timer.timeout.connect(_refresh_player_state)
add_child(resources_timer) add_child(resources_timer)
_build_refresh_dialog()
_build_escape_menu() _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: func _on_tile_hovered(q: int, r: int, terrain: String) -> void:
if coordinate_label: if coordinate_label:
coordinate_label.text = "Hex q:%d r:%d | %s" % [q, r, terrain] 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_is_home = _selected_tile_is_home()
selected_can_build_log_camp = _can_build_log_camp_on_selected_tile() selected_can_build_log_camp = _can_build_log_camp_on_selected_tile()
selected_can_build_mine = _can_build_mine_on_selected_tile() selected_can_build_mine = _can_build_mine_on_selected_tile()
if train_button: selected_can_build_barracks = _can_build_barracks_on_selected_tile()
train_button.visible = selected_is_home _update_action_buttons()
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
if coordinate_label: if coordinate_label:
coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain] 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") 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: func _can_build_on_selected_tile(required_terrain: String) -> bool:
if not grid_view: if not grid_view:
return false return false
@@ -343,13 +436,31 @@ func _can_build_on_selected_tile(required_terrain: String) -> bool:
var username: String = str(session.get("username", "")) var username: String = str(session.get("username", ""))
var owner_username: String = str(location.get("ownerUsername", "")) var owner_username: String = str(location.get("ownerUsername", ""))
var terrain: String = str(location.get("terrain", "")) 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: 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: func _selected_tile_is_home() -> bool:
@@ -391,6 +502,97 @@ func _load_session() -> Dictionary:
return {} 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: func _assign_home_location() -> void:
var access_token: String = str(session.get("accessToken", "")) var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty(): if access_token.is_empty():
@@ -484,7 +686,8 @@ func _refresh_player_state() -> void:
func _load_resources() -> void: func _load_resources() -> void:
var access_token: String = str(session.get("accessToken", "")) var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty(): if access_token.is_empty():
gold_label.text = "Gold: --" resource_text = "Gold: --\nWood: --\nOre: --"
gold_label.text = resource_text
return return
if resources_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: if resources_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
@@ -498,17 +701,20 @@ func _load_resources() -> void:
HTTPClient.METHOD_GET HTTPClient.METHOD_GET
) )
if error != OK: 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: 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: 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 return
var parsed = JSON.parse_string(body.get_string_from_utf8()) var parsed = JSON.parse_string(body.get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY: if typeof(parsed) != TYPE_DICTIONARY:
gold_label.text = "Gold: ?" coordinate_label.text = "Resource service returned invalid JSON."
return return
var resources: Dictionary = parsed 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 wood_per_minute: int = int(resources.get("woodPerMinute", 0))
var ore: int = int(resources.get("ore", 0)) var ore: int = int(resources.get("ore", 0))
var ore_per_minute: int = int(resources.get("orePerMinute", 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", "")) var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text) 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)) completed_scout_count = int(units.get("completedScouts", 0))
unit_label.text = "Scouts: %d | Queue: %d" % [completed_scout_count, training.size()] unit_label.text = "Scouts: %d | Queue: %d" % [completed_scout_count, training.size()]
if claim_button: _update_action_buttons()
claim_button.disabled = not _can_claim_selected_tile()
if training.is_empty(): if training.is_empty():
training_started_unix = 0.0 training_started_unix = 0.0
@@ -640,6 +846,9 @@ func _claim_selected_tile() -> void:
if completed_scout_count <= 0: if completed_scout_count <= 0:
coordinate_label.text = "Train a scout before claiming territory." coordinate_label.text = "Train a scout before claiming territory."
return 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", "")) var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty(): if access_token.is_empty():
@@ -661,7 +870,7 @@ func _claim_selected_tile() -> void:
JSON.stringify({ "q": selected_q, "r": selected_r }) JSON.stringify({ "q": selected_q, "r": selected_r })
) )
if error != OK: if error != OK:
claim_button.disabled = false _update_action_buttons()
coordinate_label.text = "Could not contact the locations service." 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() var text: String = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS: if result != HTTPRequest.RESULT_SUCCESS:
coordinate_label.text = "Scout claim request failed." coordinate_label.text = "Scout claim request failed."
claim_button.disabled = not selected_can_claim _update_action_buttons()
return return
if response_code < 200 or response_code >= 300: if response_code < 200 or response_code >= 300:
coordinate_label.text = "Scout claim failed: %s" % text coordinate_label.text = "Scout claim failed: %s" % text
claim_button.disabled = not selected_can_claim _update_action_buttons()
_refresh_player_state() _refresh_player_state()
return return
selected_can_claim = false 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] coordinate_label.text = "Scout claimed q:%d r:%d. Territory expanded." % [selected_q, selected_r]
_load_visible_locations() _load_visible_locations()
_refresh_player_state() _refresh_player_state()
@@ -701,6 +910,14 @@ func _build_mine() -> void:
_request_building("Mine") _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: func _request_building(building_type: String) -> void:
var access_token: String = str(session.get("accessToken", "")) var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty(): 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: if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return return
if building_type == "Mine": match building_type:
build_mine_button.disabled = true "Mine":
else: build_mine_button.disabled = true
build_log_camp_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] coordinate_label.text = "Building %s at q:%d r:%d..." % [building_type, selected_q, selected_r]
var error: int = build_request.request( 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 }) JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": building_type })
) )
if error != OK: if error != OK:
build_log_camp_button.disabled = not selected_can_build_log_camp _update_action_buttons()
build_mine_button.disabled = not selected_can_build_mine
coordinate_label.text = "Could not contact the locations service." 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() var text: String = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS: if result != HTTPRequest.RESULT_SUCCESS:
coordinate_label.text = "Build request failed." coordinate_label.text = "Build request failed."
build_log_camp_button.disabled = not selected_can_build_log_camp _update_action_buttons()
build_mine_button.disabled = not selected_can_build_mine
return return
if response_code < 200 or response_code >= 300: if response_code < 200 or response_code >= 300:
coordinate_label.text = "Build failed: %s" % text coordinate_label.text = "Build failed: %s" % text
build_log_camp_button.disabled = not selected_can_build_log_camp _update_action_buttons()
build_mine_button.disabled = not selected_can_build_mine
_refresh_player_state() _refresh_player_state()
return return
selected_can_build_log_camp = false selected_can_build_log_camp = false
selected_can_build_mine = false selected_can_build_mine = false
build_log_camp_button.disabled = true selected_can_build_barracks = false
build_mine_button.disabled = true _update_action_buttons()
var parsed = JSON.parse_string(text) var parsed = JSON.parse_string(text)
if typeof(parsed) == TYPE_DICTIONARY: if typeof(parsed) == TYPE_DICTIONARY:
var location: Dictionary = parsed 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_started_unix = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", "")))
building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", ""))) building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", "")))
building_completion_refresh_requested = false building_completion_refresh_requested = false
@@ -787,10 +1004,13 @@ func _update_training_bar() -> void:
return return
if training_started_unix <= 0.0 or training_completes_unix <= 0.0: if training_started_unix <= 0.0 or training_completes_unix <= 0.0:
training_box.visible = false
training_bar.value = 0.0 training_bar.value = 0.0
training_label.text = "Training: idle" training_label.text = "Training: idle"
_update_progress_panel_visibility()
return return
training_box.visible = true
var now: float = Time.get_unix_time_from_system() var now: float = Time.get_unix_time_from_system()
var duration: float = max(1.0, training_completes_unix - training_started_unix) var duration: float = max(1.0, training_completes_unix - training_started_unix)
var elapsed: float = clamp(now - training_started_unix, 0.0, duration) var elapsed: float = clamp(now - training_started_unix, 0.0, duration)
@@ -804,6 +1024,7 @@ func _update_training_bar() -> void:
_load_units() _load_units()
else: else:
training_label.text = "Training: %ds" % remaining training_label.text = "Training: %ds" % remaining
_update_progress_panel_visibility()
func _update_building_timer() -> void: func _update_building_timer() -> void:
@@ -811,10 +1032,13 @@ func _update_building_timer() -> void:
return return
if building_started_unix <= 0.0 or building_completes_unix <= 0.0: if building_started_unix <= 0.0 or building_completes_unix <= 0.0:
building_box.visible = false
building_bar.value = 0.0 building_bar.value = 0.0
building_label.text = "Build: idle" building_label.text = "Build: idle"
_update_progress_panel_visibility()
return return
building_box.visible = true
var now: float = 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 duration: float = max(1.0, building_completes_unix - building_started_unix)
var elapsed: float = clamp(now - building_started_unix, 0.0, duration) 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 building_completion_refresh_requested = true
_load_visible_locations() _load_visible_locations()
_refresh_player_state() _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: 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 var location: Dictionary = item
if str(location.get("ownerUsername", "")) != username: if str(location.get("ownerUsername", "")) != username:
continue continue
var building_type: String = str(location.get("buildingType", "")) var building_type: String = _nullable_string(location.get("buildingType", ""))
if building_type.is_empty(): if building_type.is_empty():
continue continue
if str(location.get("buildingStatus", "")) != "Building": if str(location.get("buildingStatus", "")) != "Building":
@@ -872,6 +1102,8 @@ func _building_display_name(building_type: String) -> String:
return "Log Camp" return "Log Camp"
"Mine": "Mine":
return "Mine" return "Mine"
"Barracks":
return "Barracks"
_: _:
return "Build" return "Build"
@@ -895,6 +1127,12 @@ func _parse_utc_timestamp(timestamp: String) -> float:
return Time.get_unix_time_from_datetime_string(normalized) 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: func _build_escape_menu() -> void:
escape_overlay = Control.new() escape_overlay = Control.new()
escape_overlay.visible = false escape_overlay.visible = false
@@ -995,8 +1233,10 @@ func _menu_button(text_value: String) -> Button:
func _small_button(text_value: String) -> Button: func _small_button(text_value: String) -> Button:
var button := _menu_button(text_value) var button := _menu_button(text_value)
button.custom_minimum_size = Vector2(128, 32) button.custom_minimum_size = Vector2(108, 32)
button.add_theme_font_size_override("font_size", 13) button.add_theme_font_size_override("font_size", 11)
button.clip_text = true
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
return button return button
@@ -161,14 +161,16 @@ func _draw_tile(tile: Dictionary) -> void:
draw_polyline(_closed_points(points), Color(1.0, 0.88, 0.38), 3.0) draw_polyline(_closed_points(points), Color(1.0, 0.88, 0.38), 3.0)
var visible_location: Dictionary = visible_tiles.get(key, {}) var visible_location: Dictionary = visible_tiles.get(key, {})
var building_type: String = str(visible_location.get("buildingType", "")) var building_type: String = _nullable_string(visible_location.get("buildingType", ""))
var building_status: String = str(visible_location.get("buildingStatus", "")) var building_status: String = _nullable_string(visible_location.get("buildingStatus", ""))
if coord == home_tile: if coord == home_tile:
_draw_town_hall_marker(center, height) _draw_town_hall_marker(center, height)
elif building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()): elif building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()):
_draw_log_camp_marker(center, height) _draw_log_camp_marker(center, height)
elif building_type == "Mine" and (building_status == "Built" or building_status.is_empty()): elif building_type == "Mine" and (building_status == "Built" or building_status.is_empty()):
_draw_mine_marker(center, height) _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(): elif not building_type.is_empty():
_draw_building_marker(center, height) _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) 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: func _draw_town_hall_marker(center: Vector2, height: float) -> void:
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0) var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0)
var width: float = 24.0 * zoom var width: float = 24.0 * zoom
@@ -434,6 +467,13 @@ func get_visible_location(q: int, r: int) -> Dictionary:
return {} 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: func _tile_key(q: int, r: int) -> String:
return "%d,%d" % [q, r] 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: func _tile_display_label(key: String, data: Dictionary) -> String:
var terrain: String = str(data.get("terrain", "Open")) var terrain: String = str(data.get("terrain", "Open"))
var building_type: String = str(data.get("buildingType", "")) var building_type: String = _nullable_string(data.get("buildingType", ""))
var building_status: String = str(data.get("buildingStatus", "")) var building_status: String = _nullable_string(data.get("buildingStatus", ""))
var building_label: String = "" var building_label: String = ""
if building_type == "LogCamp": if building_type == "LogCamp":
building_label = " | Log Camp" if building_status == "Built" or building_status.is_empty() else " | Log Camp building" building_label = " | Log Camp" if building_status == "Built" or building_status.is_empty() else " | Log Camp building"
elif building_type == "Mine": elif building_type == "Mine":
building_label = " | Mine" if building_status == "Built" or building_status.is_empty() else " | Mine building" 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))): if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))):
return "%s | Your home%s" % [terrain, building_label] return "%s | Your home%s" % [terrain, building_label]
@@ -509,6 +551,12 @@ func _owner_username(location: Dictionary) -> String:
return str(owner_value) 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: func _tile_height(q: int, r: int) -> float:
var distance: int = max(max(abs(q), abs(r)), abs(-q - r)) var distance: int = max(max(abs(q), abs(r)), abs(-q - r))
if distance <= 1: if distance <= 1:
@@ -88,6 +88,11 @@ public class LocationsController : ControllerBase
return NotFound(); 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); var existingLocation = await _locations.GetAsync(request.Q, request.R);
if (existingLocation is not null && !string.IsNullOrWhiteSpace(existingLocation.OwnerUsername)) if (existingLocation is not null && !string.IsNullOrWhiteSpace(existingLocation.OwnerUsername))
{ {
+1 -1
View File
@@ -52,7 +52,7 @@ public class LocationResponse
Label = location.Label, Label = location.Label,
Level = location.Level, Level = location.Level,
BuildingType = location.BuildingType, 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" ? "Built"
: location.BuildingStatus, : location.BuildingStatus,
BuildingStartedAtUtc = location.BuildingStartedAtUtc, BuildingStartedAtUtc = location.BuildingStartedAtUtc,
+5 -2
View File
@@ -44,8 +44,10 @@ training record.
Buildings are stored on location records. `LogCamp` can be built on controlled 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 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 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 build, and adds 5 ore per minute after it finishes. `Barracks` can be built on
building slot; a building under construction occupies that slot. 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 Timer settings are stored in the MongoDB `GameSettings` collection and are
seeded on first read: seeded on first read:
@@ -53,3 +55,4 @@ seeded on first read:
- `ScoutTrainingSeconds` - `ScoutTrainingSeconds`
- `LogCampBuildSeconds` - `LogCampBuildSeconds`
- `MineBuildSeconds` - `MineBuildSeconds`
- `BarracksBuildSeconds`
@@ -8,6 +8,7 @@ public class GameSettingsService
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds"; public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
public const string LogCampBuildSeconds = "LogCampBuildSeconds"; public const string LogCampBuildSeconds = "LogCampBuildSeconds";
public const string MineBuildSeconds = "MineBuildSeconds"; public const string MineBuildSeconds = "MineBuildSeconds";
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
private readonly IMongoCollection<GameSetting> _settings; private readonly IMongoCollection<GameSetting> _settings;
@@ -15,6 +15,10 @@ public class LocationService
private readonly int _mineOrePerMinute; private readonly int _mineOrePerMinute;
private readonly int _defaultLogCampBuildSeconds; private readonly int _defaultLogCampBuildSeconds;
private readonly int _defaultMineBuildSeconds; 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) public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
{ {
@@ -40,6 +44,18 @@ public class LocationService
_defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds) _defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds)
? Math.Max(1, mineBuildSeconds) ? Math.Max(1, mineBuildSeconds)
: 60; : 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<Location>.IndexKeys var coordinateIndex = Builders<Location>.IndexKeys
.Ascending(x => x.Q) .Ascending(x => x.Q)
@@ -149,8 +165,8 @@ public class LocationService
throw new InvalidOperationException("This location already has a building or construction in progress."); throw new InvalidOperationException("This location already has a building or construction in progress.");
} }
var requiredTerrain = buildingType == "LogCamp" ? "Forest" : "Ore"; var requiredTerrain = RequiredTerrainForBuilding(buildingType);
if (!string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase)) if (requiredTerrain is not null && !string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
{ {
throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory."); throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory.");
} }
@@ -158,12 +174,8 @@ public class LocationService
await CompleteFinishedBuildingsAsync(username); await CompleteFinishedBuildingsAsync(username);
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var settingKey = buildingType == "LogCamp" var settingKey = BuildSecondsSettingKey(buildingType);
? GameSettingsService.LogCampBuildSeconds var defaultBuildSeconds = DefaultBuildSeconds(buildingType);
: GameSettingsService.MineBuildSeconds;
var defaultBuildSeconds = buildingType == "LogCamp"
? _defaultLogCampBuildSeconds
: _defaultMineBuildSeconds;
var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds); var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds);
var update = Builders<Location>.Update var update = Builders<Location>.Update
.Set(x => x.BuildingType, buildingType) .Set(x => x.BuildingType, buildingType)
@@ -173,7 +185,7 @@ public class LocationService
.Set(x => x.UpdatedAtUtc, now); .Set(x => x.UpdatedAtUtc, now);
var updated = await _locations.FindOneAndUpdateAsync( 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, update,
new FindOneAndUpdateOptions<Location> new FindOneAndUpdateOptions<Location>
{ {
@@ -185,6 +197,27 @@ public class LocationService
throw new InvalidOperationException("This location already has a building or construction in progress."); 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<Location>.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; return updated;
} }
@@ -193,7 +226,7 @@ public class LocationService
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var readyBuildings = await _locations.Find(x => var readyBuildings = await _locations.Find(x =>
x.OwnerUsername == username && x.OwnerUsername == username &&
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine") && (x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Barracks") &&
x.BuildingStatus == "Building" && x.BuildingStatus == "Building" &&
x.BuildingCompletesAtUtc <= now) x.BuildingCompletesAtUtc <= now)
.ToListAsync(); .ToListAsync();
@@ -353,6 +386,15 @@ public class LocationService
return ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth); return ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth);
} }
public async Task<bool> 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<Location?> TryCreateHomeAsync(string username, int q, int r) private async Task<Location?> TryCreateHomeAsync(string username, int q, int r)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@@ -582,7 +624,35 @@ public class LocationService
{ {
"logcamp" or "log camp" => "LogCamp", "logcamp" or "log camp" => "LogCamp",
"mine" => "Mine", "mine" => "Mine",
"barracks" => "Barracks",
_ => null _ => 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
};
} }
@@ -82,6 +82,35 @@ public class ResourceService
return updated is not null; return updated is not null;
} }
public async Task<bool> 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<PlayerResources>.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<PlayerResources>
{
ReturnDocument = ReturnDocument.After
});
return updated is not null;
}
public async Task AddWoodGenerationAsync(string username, int woodPerMinute) public async Task AddWoodGenerationAsync(string username, int woodPerMinute)
{ {
if (woodPerMinute <= 0) if (woodPerMinute <= 0)
+5 -1
View File
@@ -31,7 +31,11 @@
"LogCampWoodPerMinute": 5, "LogCampWoodPerMinute": 5,
"LogCampBuildSeconds": 60, "LogCampBuildSeconds": 60,
"MineOrePerMinute": 5, "MineOrePerMinute": 5,
"MineBuildSeconds": 60 "MineBuildSeconds": 60,
"BarracksBuildSeconds": 60,
"BarracksGoldCost": 500,
"BarracksWoodCost": 100,
"BarracksOreCost": 100
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }
@@ -19,3 +19,7 @@ data:
Buildings__LogCampBuildSeconds: "60" Buildings__LogCampBuildSeconds: "60"
Buildings__MineOrePerMinute: "5" Buildings__MineOrePerMinute: "5"
Buildings__MineBuildSeconds: "60" Buildings__MineBuildSeconds: "60"
Buildings__BarracksBuildSeconds: "60"
Buildings__BarracksGoldCost: "500"
Buildings__BarracksWoodCost: "100"
Buildings__BarracksOreCost: "100"