allowing territory to be abandoned, units to be dismissed, and buildings to be destroyed

This commit is contained in:
hz
2026-05-20 19:38:33 -05:00
parent 4a3d3af41c
commit f142440aca
7 changed files with 445 additions and 16 deletions
@@ -16,9 +16,11 @@ var resources_request: HTTPRequest
var units_request: HTTPRequest var units_request: HTTPRequest
var train_unit_request: HTTPRequest var train_unit_request: HTTPRequest
var move_units_request: HTTPRequest var move_units_request: HTTPRequest
var dismiss_units_request: HTTPRequest
var worker_request: HTTPRequest var worker_request: HTTPRequest
var claim_request: HTTPRequest var claim_request: HTTPRequest
var build_request: HTTPRequest var build_request: HTTPRequest
var territory_action_request: HTTPRequest
var refresh_request: HTTPRequest var refresh_request: HTTPRequest
var resources_timer: Timer var resources_timer: Timer
var auth_check_timer: Timer var auth_check_timer: Timer
@@ -58,10 +60,14 @@ var build_log_camp_button: Button
var build_mine_button: Button var build_mine_button: Button
var build_farm_button: Button var build_farm_button: Button
var build_barracks_button: Button var build_barracks_button: Button
var destroy_building_button: Button
var abandon_territory_button: Button
var unit_action_popup: PopupPanel var unit_action_popup: PopupPanel
var unit_action_move_button: Button var unit_action_move_button: Button
var unit_action_claim_button: Button
var unit_action_assign_button: Button var unit_action_assign_button: Button
var unit_action_deassign_button: Button var unit_action_deassign_button: Button
var unit_action_dismiss_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
@@ -69,6 +75,8 @@ var selected_can_build_log_camp := false
var selected_can_build_mine := false var selected_can_build_mine := false
var selected_can_build_farm := false var selected_can_build_farm := false
var selected_can_build_barracks := false var selected_can_build_barracks := false
var selected_can_destroy_building := false
var selected_can_abandon_territory := false
var selected_can_train_archer := false var selected_can_train_archer := false
var selected_is_home := false var selected_is_home := false
var completed_scout_count := 0 var completed_scout_count := 0
@@ -266,6 +274,16 @@ func _build_ui() -> void:
build_barracks_button.pressed.connect(_build_barracks) build_barracks_button.pressed.connect(_build_barracks)
action_buttons.add_child(build_barracks_button) action_buttons.add_child(build_barracks_button)
destroy_building_button = _small_button("Destroy Building")
destroy_building_button.visible = false
destroy_building_button.pressed.connect(_destroy_selected_building)
action_buttons.add_child(destroy_building_button)
abandon_territory_button = _small_button("Give Up")
abandon_territory_button.visible = false
abandon_territory_button.pressed.connect(_abandon_selected_territory)
action_buttons.add_child(abandon_territory_button)
unit_label = Label.new() unit_label = Label.new()
unit_label.text = "Scouts: -- | Archers: -- | Queue: --" unit_label.text = "Scouts: -- | Archers: -- | Queue: --"
unit_label.custom_minimum_size = Vector2(180, 0) unit_label.custom_minimum_size = Vector2(180, 0)
@@ -297,6 +315,10 @@ func _build_ui() -> void:
move_units_request.request_completed.connect(_on_move_units_request_completed) move_units_request.request_completed.connect(_on_move_units_request_completed)
add_child(move_units_request) add_child(move_units_request)
dismiss_units_request = HTTPRequest.new()
dismiss_units_request.request_completed.connect(_on_dismiss_units_request_completed)
add_child(dismiss_units_request)
worker_request = HTTPRequest.new() worker_request = HTTPRequest.new()
worker_request.request_completed.connect(_on_worker_request_completed) worker_request.request_completed.connect(_on_worker_request_completed)
add_child(worker_request) add_child(worker_request)
@@ -309,6 +331,10 @@ 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)
territory_action_request = HTTPRequest.new()
territory_action_request.request_completed.connect(_on_territory_action_request_completed)
add_child(territory_action_request)
refresh_request = HTTPRequest.new() refresh_request = HTTPRequest.new()
refresh_request.request_completed.connect(_on_refresh_request_completed) refresh_request.request_completed.connect(_on_refresh_request_completed)
add_child(refresh_request) add_child(refresh_request)
@@ -482,6 +508,11 @@ func _build_unit_action_menu() -> void:
unit_action_move_button.pressed.connect(_confirm_unit_move) unit_action_move_button.pressed.connect(_confirm_unit_move)
actions.add_child(unit_action_move_button) actions.add_child(unit_action_move_button)
unit_action_claim_button = _small_button("Claim")
unit_action_claim_button.custom_minimum_size = Vector2(92, 34)
unit_action_claim_button.pressed.connect(_confirm_scout_claim)
actions.add_child(unit_action_claim_button)
unit_action_assign_button = _small_button("Assign") unit_action_assign_button = _small_button("Assign")
unit_action_assign_button.custom_minimum_size = Vector2(92, 34) unit_action_assign_button.custom_minimum_size = Vector2(92, 34)
unit_action_assign_button.pressed.connect(_confirm_worker_assign) unit_action_assign_button.pressed.connect(_confirm_worker_assign)
@@ -492,6 +523,11 @@ func _build_unit_action_menu() -> void:
unit_action_deassign_button.pressed.connect(_confirm_worker_deassign) unit_action_deassign_button.pressed.connect(_confirm_worker_deassign)
actions.add_child(unit_action_deassign_button) actions.add_child(unit_action_deassign_button)
unit_action_dismiss_button = _small_button("Dismiss")
unit_action_dismiss_button.custom_minimum_size = Vector2(92, 34)
unit_action_dismiss_button.pressed.connect(_confirm_unit_dismiss)
actions.add_child(unit_action_dismiss_button)
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:
@@ -510,6 +546,8 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
selected_can_build_mine = _can_build_mine_on_selected_tile() selected_can_build_mine = _can_build_mine_on_selected_tile()
selected_can_build_farm = _can_build_farm_on_selected_tile() selected_can_build_farm = _can_build_farm_on_selected_tile()
selected_can_build_barracks = _can_build_barracks_on_selected_tile() selected_can_build_barracks = _can_build_barracks_on_selected_tile()
selected_can_destroy_building = _can_destroy_building_on_selected_tile()
selected_can_abandon_territory = _can_abandon_selected_tile()
selected_can_train_archer = _selected_tile_has_built_barracks() selected_can_train_archer = _selected_tile_has_built_barracks()
_update_action_buttons() _update_action_buttons()
if coordinate_label: if coordinate_label:
@@ -577,27 +615,22 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
var has_worker := not _nullable_string(location.get("workerUnitId", "")).is_empty() var has_worker := not _nullable_string(location.get("workerUnitId", "")).is_empty()
var selected_peasants := selected_peasant_count > 0 var selected_peasants := selected_peasant_count > 0
var selected_scouts := selected_scout_count > 0 var selected_scouts := selected_scout_count > 0
if selected_archer_count <= 0 and not selected_scouts and not selected_peasants and not has_worker: var selected_archers := selected_archer_count > 0
var can_scout_claim := selected_scouts and _can_claim_tile(q, r)
var is_selected_unit_tile := (selected_archers and q == selected_archer_q and r == selected_archer_r) or (selected_scouts and q == selected_scout_q and r == selected_scout_r) or (selected_peasants and q == selected_peasant_q and r == selected_peasant_r)
if not selected_archers and not selected_scouts and not selected_peasants and not has_worker:
if unit_action_popup: if unit_action_popup:
unit_action_popup.hide() unit_action_popup.hide()
if coordinate_label: if coordinate_label:
coordinate_label.text = "Select a unit or right-click a staffed building." coordinate_label.text = "Select a unit or right-click a staffed building."
return return
if selected_archer_count > 0 and q == selected_archer_q and r == selected_archer_r: if selected_peasants and not is_selected_unit_tile and not grid_view.is_owned_tile(q, r):
if coordinate_label:
coordinate_label.text = "Archer squad is already on that tile."
return
if selected_scouts and q == selected_scout_q and r == selected_scout_r:
if coordinate_label:
coordinate_label.text = "Scout squad is already on that tile."
return
if selected_peasants and not grid_view.is_owned_tile(q, r):
if coordinate_label: if coordinate_label:
coordinate_label.text = "Peasants can only move within controlled territory." coordinate_label.text = "Peasants can only move within controlled territory."
return return
if not grid_view.is_visible_tile(q, r): if not is_selected_unit_tile and not grid_view.is_visible_tile(q, r):
if coordinate_label: if coordinate_label:
coordinate_label.text = "Archers can only move to visible tiles." coordinate_label.text = "Units can only move to visible tiles."
return return
pending_move_q = q pending_move_q = q
@@ -605,10 +638,19 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
if coordinate_label: if coordinate_label:
coordinate_label.text = "Orders for q:%d r:%d." % [q, r] coordinate_label.text = "Orders for q:%d r:%d." % [q, r]
if unit_action_popup: if unit_action_popup:
unit_action_move_button.visible = selected_archer_count > 0 or selected_scouts or selected_peasants unit_action_move_button.visible = not is_selected_unit_tile and (selected_archers or selected_scouts or selected_peasants)
unit_action_claim_button.visible = can_scout_claim
unit_action_assign_button.visible = not selected_peasant_id.is_empty() and has_building and not has_worker unit_action_assign_button.visible = not selected_peasant_id.is_empty() and has_building and not has_worker
unit_action_deassign_button.visible = has_worker unit_action_deassign_button.visible = has_worker
var popup_size := Vector2i(116, 132) unit_action_dismiss_button.visible = is_selected_unit_tile and (selected_archers or selected_scouts or selected_peasants)
var visible_action_count := 0
for button in [unit_action_move_button, unit_action_claim_button, unit_action_assign_button, unit_action_deassign_button, unit_action_dismiss_button]:
if button.visible:
visible_action_count += 1
if visible_action_count == 0:
unit_action_popup.hide()
return
var popup_size := Vector2i(116, 16 + visible_action_count * 40)
var visible_rect := get_viewport().get_visible_rect() var visible_rect := get_viewport().get_visible_rect()
var max_x := int(max(0.0, visible_rect.size.x - popup_size.x - 8.0)) var max_x := int(max(0.0, visible_rect.size.x - popup_size.x - 8.0))
var max_y := int(max(0.0, visible_rect.size.y - popup_size.y - 8.0)) var max_y := int(max(0.0, visible_rect.size.y - popup_size.y - 8.0))
@@ -629,6 +671,20 @@ func _confirm_unit_move() -> void:
_move_selected_peasants(pending_move_q, pending_move_r) _move_selected_peasants(pending_move_q, pending_move_r)
func _confirm_scout_claim() -> void:
if unit_action_popup:
unit_action_popup.hide()
if pending_move_q == 9999:
return
if not _can_claim_tile(pending_move_q, pending_move_r):
coordinate_label.text = "Scouts can only claim visible unclaimed territory adjacent to controlled territory."
return
selected_q = pending_move_q
selected_r = pending_move_r
selected_can_claim = true
_claim_selected_tile()
func _confirm_worker_assign() -> void: func _confirm_worker_assign() -> void:
if unit_action_popup: if unit_action_popup:
unit_action_popup.hide() unit_action_popup.hide()
@@ -645,6 +701,17 @@ func _confirm_worker_deassign() -> void:
_request_worker_action("deassign-worker", { "q": pending_move_q, "r": pending_move_r }) _request_worker_action("deassign-worker", { "q": pending_move_q, "r": pending_move_r })
func _confirm_unit_dismiss() -> void:
if unit_action_popup:
unit_action_popup.hide()
if selected_archer_count > 0:
_request_unit_dismiss("Archer", selected_archer_q, selected_archer_r)
elif selected_scout_count > 0:
_request_unit_dismiss("Scout", selected_scout_q, selected_scout_r)
elif selected_peasant_count > 0:
_request_unit_dismiss("Peasant", selected_peasant_q, selected_peasant_r)
func _location_has_completed_building(location: Dictionary) -> bool: func _location_has_completed_building(location: Dictionary) -> bool:
if location.is_empty(): if location.is_empty():
return false return false
@@ -675,6 +742,36 @@ func _can_build_barracks_on_selected_tile() -> bool:
return kind != "Home" return kind != "Home"
func _can_destroy_building_on_selected_tile() -> bool:
if not grid_view:
return false
var location: Dictionary = grid_view.get_visible_location(selected_q, selected_r)
if location.is_empty():
return false
var username: String = str(session.get("username", ""))
var owner_username: String = str(location.get("ownerUsername", ""))
var kind: String = str(location.get("kind", ""))
var building_type: String = _nullable_string(location.get("buildingType", ""))
return owner_username == username and kind != "Home" and not building_type.is_empty()
func _can_abandon_selected_tile() -> bool:
if not grid_view:
return false
var location: Dictionary = grid_view.get_visible_location(selected_q, selected_r)
if location.is_empty():
return false
var username: String = str(session.get("username", ""))
var owner_username: String = str(location.get("ownerUsername", ""))
var kind: String = str(location.get("kind", ""))
var building_type: String = _nullable_string(location.get("buildingType", ""))
return owner_username == username and kind != "Home" and building_type.is_empty()
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
@@ -695,6 +792,22 @@ func _can_claim_selected_tile() -> bool:
return selected_can_claim and completed_scout_count > 0 and grid_view and grid_view.has_owned_neighbor(selected_q, selected_r) return selected_can_claim and completed_scout_count > 0 and grid_view and grid_view.has_owned_neighbor(selected_q, selected_r)
func _can_claim_tile(q: int, r: int) -> bool:
if not grid_view:
return false
if completed_scout_count <= 0:
return false
if not grid_view.is_visible_tile(q, r):
return false
var location: Dictionary = grid_view.get_visible_location(q, r)
if location.is_empty():
return false
var owner_username: String = _nullable_string(location.get("ownerUsername", ""))
return owner_username.is_empty() and grid_view.has_owned_neighbor(q, r)
func _update_action_buttons() -> void: func _update_action_buttons() -> void:
if train_button: if train_button:
train_button.visible = selected_is_home train_button.visible = selected_is_home
@@ -720,6 +833,12 @@ func _update_action_buttons() -> void:
if build_barracks_button: if build_barracks_button:
build_barracks_button.visible = selected_can_build_barracks build_barracks_button.visible = selected_can_build_barracks
build_barracks_button.disabled = false build_barracks_button.disabled = false
if destroy_building_button:
destroy_building_button.visible = selected_can_destroy_building
destroy_building_button.disabled = false
if abandon_territory_button:
abandon_territory_button.visible = selected_can_abandon_territory
abandon_territory_button.disabled = false
func _clear_selected_archers() -> void: func _clear_selected_archers() -> void:
@@ -1356,6 +1475,47 @@ func _on_move_units_request_completed(result: int, response_code: int, _headers:
_load_units() _load_units()
func _request_unit_dismiss(unit_type: String, q: int, r: int) -> void:
var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty():
coordinate_label.text = "No auth token available. Unit squad was not dismissed."
return
if dismiss_units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
coordinate_label.text = "Dismissing %s squad at q:%d r:%d..." % [unit_type, q, r]
var error: int = dismiss_units_request.request(
LOCATIONS_BASE_URL + "/api/units/dismiss",
PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % access_token
]),
HTTPClient.METHOD_POST,
JSON.stringify({ "unitType": unit_type, "q": q, "r": r })
)
if error != OK:
coordinate_label.text = "Could not contact the unit dismiss service."
func _on_dismiss_units_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var text: String = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS:
coordinate_label.text = "Unit dismiss request failed."
return
if response_code < 200 or response_code >= 300:
coordinate_label.text = "Unit dismiss failed: %s" % text
_refresh_player_state()
return
_clear_selected_archers()
coordinate_label.text = "Unit squad dismissed."
_load_visible_locations()
_load_units()
_refresh_player_state()
func _request_worker_action(endpoint: String, body: Dictionary) -> void: func _request_worker_action(endpoint: String, body: Dictionary) -> 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():
@@ -1480,6 +1640,70 @@ func _build_barracks() -> void:
_request_building("Barracks") _request_building("Barracks")
func _destroy_selected_building() -> void:
if not selected_can_destroy_building:
coordinate_label.text = "Select controlled territory with a non-town-hall building."
return
_request_territory_action("destroy-building", "Destroying building")
func _abandon_selected_territory() -> void:
if not selected_can_abandon_territory:
coordinate_label.text = "Select controlled territory without a building."
return
_request_territory_action("abandon", "Giving up territory")
func _request_territory_action(endpoint: String, message: String) -> void:
var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty():
coordinate_label.text = "No auth token available. Territory action failed."
return
if territory_action_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
destroy_building_button.disabled = true
abandon_territory_button.disabled = true
coordinate_label.text = "%s at q:%d r:%d..." % [message, selected_q, selected_r]
var error: int = territory_action_request.request(
LOCATIONS_BASE_URL + "/api/locations/" + endpoint,
PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % access_token
]),
HTTPClient.METHOD_POST,
JSON.stringify({ "q": selected_q, "r": selected_r })
)
if error != OK:
_update_action_buttons()
coordinate_label.text = "Could not contact the locations service."
func _on_territory_action_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var text: String = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS:
coordinate_label.text = "Territory action request failed."
_update_action_buttons()
return
if response_code < 200 or response_code >= 300:
coordinate_label.text = "Territory action failed: %s" % text
_update_action_buttons()
_refresh_player_state()
return
selected_can_destroy_building = false
selected_can_abandon_territory = false
_update_action_buttons()
coordinate_label.text = "Territory updated."
_load_visible_locations()
_load_units()
_refresh_player_state()
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():
@@ -1529,7 +1753,10 @@ func _on_build_request_completed(result: int, response_code: int, _headers: Pack
selected_can_build_log_camp = false selected_can_build_log_camp = false
selected_can_build_mine = false selected_can_build_mine = false
selected_can_build_farm = false
selected_can_build_barracks = false selected_can_build_barracks = false
selected_can_destroy_building = true
selected_can_abandon_territory = false
_update_action_buttons() _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:
@@ -180,6 +180,52 @@ public class LocationsController : ControllerBase
: Ok(LocationResponse.FromLocation(location)); : Ok(LocationResponse.FromLocation(location));
} }
[Authorize(Roles = "USER,SUPER")]
[HttpPost("destroy-building")]
public async Task<ActionResult<LocationResponse>> DestroyBuilding([FromBody] TerritoryLocationRequest request)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
try
{
var location = await _locations.DestroyBuildingAsync(username, request.Q, request.R);
return location is null
? BadRequest("Select controlled territory with a non-town-hall building.")
: Ok(LocationResponse.FromLocation(location));
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[Authorize(Roles = "USER,SUPER")]
[HttpPost("abandon")]
public async Task<ActionResult<LocationResponse>> Abandon([FromBody] TerritoryLocationRequest request)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
try
{
var location = await _locations.AbandonAsync(username, request.Q, request.R);
return location is null
? BadRequest("Select controlled non-home territory to give up.")
: Ok(LocationResponse.FromLocation(location));
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[Authorize(Roles = "USER,SUPER")] [Authorize(Roles = "USER,SUPER")]
[HttpGet("home")] [HttpGet("home")]
public async Task<ActionResult<LocationResponse>> GetHome() public async Task<ActionResult<LocationResponse>> GetHome()
@@ -89,4 +89,34 @@ public class UnitsController : ControllerBase
return BadRequest(new { error = ex.Message }); return BadRequest(new { error = ex.Message });
} }
} }
[Authorize(Roles = "USER,SUPER")]
[HttpPost("dismiss")]
public async Task<ActionResult<DismissUnitSquadResponse>> Dismiss([FromBody] DismissUnitSquadRequest request)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
try
{
var dismissedCount = await _units.DismissSquadAsync(username, request);
if (dismissedCount == 0)
{
return BadRequest(new { error = "No matching idle units are available to dismiss." });
}
return Ok(new DismissUnitSquadResponse
{
UnitType = request.UnitType,
DismissedCount = dismissedCount
});
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
} }
+6
View File
@@ -35,6 +35,12 @@ public class DeassignWorkerRequest
public int R { get; set; } public int R { get; set; }
} }
public class TerritoryLocationRequest
{
public int Q { get; set; }
public int R { get; set; }
}
public class LocationResponse public class LocationResponse
{ {
public string Id { get; set; } = ""; public string Id { get; set; } = "";
@@ -17,6 +17,19 @@ public class MoveUnitSquadRequest
public int ToR { get; set; } public int ToR { get; set; }
} }
public class DismissUnitSquadRequest
{
public string UnitType { get; set; } = "Archer";
public int Q { get; set; }
public int R { get; set; }
}
public class DismissUnitSquadResponse
{
public string UnitType { get; set; } = "";
public int DismissedCount { get; set; }
}
public class UnitTrainingResponse public class UnitTrainingResponse
{ {
public string Id { get; set; } = ""; public string Id { get; set; } = "";
@@ -355,6 +355,93 @@ public class LocationService
return updated; return updated;
} }
public async Task<Location?> DestroyBuildingAsync(string username, int q, int r)
{
var now = DateTime.UtcNow;
var location = await GetAsync(q, r);
if (location is null ||
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrWhiteSpace(location.BuildingType))
{
return null;
}
if (string.Equals(location.Kind, "Home", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The town hall cannot be destroyed.");
}
if (!string.IsNullOrWhiteSpace(location.WorkerUnitId))
{
await _units.UpdateOneAsync(
x => x.Id == location.WorkerUnitId && x.Username == username,
Builders<UnitInstance>.Update
.Set(x => x.Q, q)
.Set(x => x.R, r)
.Set(x => x.Status, "Idle")
.Set(x => x.MovementRemaining, 1)
.Set(x => x.UpdatedAtUtc, now));
}
var updated = await _locations.FindOneAndUpdateAsync(
x => x.Id == location.Id && x.OwnerUsername == username && x.BuildingType != null && x.BuildingType != "",
Builders<Location>.Update
.Unset(x => x.BuildingType)
.Unset(x => x.BuildingStatus)
.Unset(x => x.BuildingStartedAtUtc)
.Unset(x => x.BuildingCompletesAtUtc)
.Unset(x => x.BuildingBuiltAtUtc)
.Unset(x => x.WorkerUnitId)
.Unset(x => x.WorkerUnitType)
.Set(x => x.StoredResourceAmount, 0)
.Set(x => x.StoredResourceCapacity, 10)
.Unset(x => x.LastResourceGeneratedAtUtc)
.Set(x => x.UpdatedAtUtc, now),
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
await RecalculateWorkerGenerationAsync(username);
return updated;
}
public async Task<Location?> AbandonAsync(string username, int q, int r)
{
var location = await GetAsync(q, r);
if (location is null ||
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (string.Equals(location.Kind, "Home", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The town hall cannot be given up.");
}
if (!string.IsNullOrWhiteSpace(location.BuildingType) || !string.IsNullOrWhiteSpace(location.WorkerUnitId))
{
throw new InvalidOperationException("Destroy the building before giving up this territory.");
}
var hasUnits = await _units.Find(x =>
x.Username == username &&
x.Q == q &&
x.R == r)
.AnyAsync();
if (hasUnits)
{
throw new InvalidOperationException("Move or dismiss units on this tile before giving up the territory.");
}
return await _locations.FindOneAndUpdateAsync(
x => x.Id == location.Id && x.OwnerUsername == username,
Builders<Location>.Update
.Set(x => x.Kind, "Open")
.Unset(x => x.OwnerUsername)
.Unset(x => x.Label)
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow),
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
}
private async Task RecalculateWorkerGenerationAsync(string username) private async Task RecalculateWorkerGenerationAsync(string username)
{ {
var built = await _locations.Find(x => var built = await _locations.Find(x =>
@@ -197,9 +197,9 @@ public class UnitTrainingService
} }
var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR); var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR);
if (normalizedUnitType == ArcherUnitType && !await CanMoveToAsync(username, request.ToQ, request.ToR)) if ((normalizedUnitType == ArcherUnitType || normalizedUnitType == ScoutUnitType) && !await CanMoveToAsync(username, request.ToQ, request.ToR))
{ {
throw new InvalidOperationException("Archers can only move to visible tiles."); throw new InvalidOperationException($"{normalizedUnitType}s can only move to visible tiles.");
} }
if (normalizedUnitType == PeasantUnitType && !await CanPeasantMoveToAsync(username, request.ToQ, request.ToR)) if (normalizedUnitType == PeasantUnitType && !await CanPeasantMoveToAsync(username, request.ToQ, request.ToR))
@@ -257,6 +257,26 @@ public class UnitTrainingService
.ToListAsync(); .ToListAsync();
} }
public async Task<int> DismissSquadAsync(string username, DismissUnitSquadRequest request)
{
var normalizedUnitType = NormalizeUnitType(request.UnitType);
if (normalizedUnitType != ScoutUnitType && normalizedUnitType != ArcherUnitType && normalizedUnitType != PeasantUnitType)
{
throw new ArgumentException("Only Scouts, Archers, and Peasants can be dismissed right now.", nameof(request.UnitType));
}
await CompletePendingMovementAsync(username);
var result = await _units.DeleteManyAsync(x =>
x.Username == username &&
x.UnitType == normalizedUnitType &&
x.Status == "Idle" &&
x.Q == request.Q &&
x.R == request.R);
return (int)result.DeletedCount;
}
private async Task<Location?> GetBuiltBarracksAsync(string username, int q, int r) private async Task<Location?> GetBuiltBarracksAsync(string username, int q, int r)
{ {
await CompleteFinishedBuildingsAsync(username); await CompleteFinishedBuildingsAsync(username);