Movement enhancements
This commit is contained in:
@@ -77,6 +77,7 @@ var completed_peasant_count := 0
|
||||
var selected_peasant_id := ""
|
||||
var selected_peasant_q := 9999
|
||||
var selected_peasant_r := 9999
|
||||
var selected_peasant_count := 0
|
||||
var selected_archer_q := 9999
|
||||
var selected_archer_r := 9999
|
||||
var selected_archer_count := 0
|
||||
@@ -514,16 +515,18 @@ func _handle_unit_selection_or_move(q: int, r: int) -> bool:
|
||||
|
||||
var archer_count: int = grid_view.get_archer_count_at(q, r)
|
||||
var peasant_id: String = grid_view.get_peasant_id_at(q, r)
|
||||
if not peasant_id.is_empty():
|
||||
var peasant_count: int = grid_view.get_peasant_count_at(q, r)
|
||||
if peasant_count > 0:
|
||||
if unit_action_popup:
|
||||
unit_action_popup.hide()
|
||||
selected_peasant_id = peasant_id
|
||||
selected_peasant_q = q
|
||||
selected_peasant_r = r
|
||||
selected_peasant_count = peasant_count
|
||||
selected_archer_count = 0
|
||||
grid_view.set_selected_unit_tile(q, r)
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Selected Peasant at q:%d r:%d. Right-click a building to assign work." % [q, r]
|
||||
coordinate_label.text = "Selected Peasant squad: %d at q:%d r:%d. Right-click controlled territory to move or a building to assign work." % [peasant_count, q, r]
|
||||
return true
|
||||
|
||||
if archer_count > 0:
|
||||
@@ -533,6 +536,7 @@ func _handle_unit_selection_or_move(q: int, r: int) -> bool:
|
||||
selected_archer_r = r
|
||||
selected_archer_count = archer_count
|
||||
selected_peasant_id = ""
|
||||
selected_peasant_count = 0
|
||||
grid_view.set_selected_unit_tile(q, r)
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Selected Archer squad: %d at q:%d r:%d. Right-click an adjacent visible tile for orders." % [archer_count, q, r]
|
||||
@@ -547,7 +551,8 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
|
||||
var location: Dictionary = grid_view.get_visible_location(q, r)
|
||||
var has_building := _location_has_completed_building(location)
|
||||
var has_worker := not _nullable_string(location.get("workerUnitId", "")).is_empty()
|
||||
if selected_archer_count <= 0 and selected_peasant_id.is_empty() and not has_worker:
|
||||
var selected_peasants := selected_peasant_count > 0
|
||||
if selected_archer_count <= 0 and not selected_peasants and not has_worker:
|
||||
if unit_action_popup:
|
||||
unit_action_popup.hide()
|
||||
if coordinate_label:
|
||||
@@ -561,6 +566,10 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Archers can only move to an adjacent tile."
|
||||
return
|
||||
if selected_peasants and not grid_view.is_owned_tile(q, r):
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Peasants can only move within controlled territory."
|
||||
return
|
||||
if not grid_view.is_visible_tile(q, r):
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Archers can only move to visible tiles."
|
||||
@@ -571,7 +580,7 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Orders for q:%d r:%d." % [q, r]
|
||||
if unit_action_popup:
|
||||
unit_action_move_button.visible = selected_archer_count > 0
|
||||
unit_action_move_button.visible = selected_archer_count > 0 or selected_peasants
|
||||
unit_action_assign_button.visible = not selected_peasant_id.is_empty() and has_building and not has_worker
|
||||
unit_action_deassign_button.visible = has_worker
|
||||
var popup_size := Vector2i(116, 132)
|
||||
@@ -587,7 +596,10 @@ func _confirm_unit_move() -> void:
|
||||
unit_action_popup.hide()
|
||||
if pending_move_q == 9999:
|
||||
return
|
||||
_move_selected_archers(pending_move_q, pending_move_r)
|
||||
if selected_archer_count > 0:
|
||||
_move_selected_archers(pending_move_q, pending_move_r)
|
||||
elif selected_peasant_count > 0:
|
||||
_move_selected_peasants(pending_move_q, pending_move_r)
|
||||
|
||||
|
||||
func _confirm_worker_assign() -> void:
|
||||
@@ -689,6 +701,7 @@ func _clear_selected_archers() -> void:
|
||||
selected_peasant_id = ""
|
||||
selected_peasant_q = 9999
|
||||
selected_peasant_r = 9999
|
||||
selected_peasant_count = 0
|
||||
pending_move_q = 9999
|
||||
pending_move_r = 9999
|
||||
if unit_action_popup:
|
||||
@@ -1205,19 +1218,57 @@ func _move_selected_archers(to_q: int, to_r: int) -> void:
|
||||
coordinate_label.text = "Could not contact the unit move service."
|
||||
|
||||
|
||||
func _move_selected_peasants(to_q: int, to_r: int) -> void:
|
||||
if selected_peasant_count <= 0:
|
||||
return
|
||||
if to_q == selected_peasant_q and to_r == selected_peasant_r:
|
||||
coordinate_label.text = "Peasant squad is already on that tile."
|
||||
return
|
||||
if not grid_view.is_owned_tile(to_q, to_r):
|
||||
coordinate_label.text = "Peasants can only move within controlled territory."
|
||||
return
|
||||
|
||||
var access_token: String = str(session.get("accessToken", ""))
|
||||
if access_token.is_empty():
|
||||
coordinate_label.text = "No auth token available. Peasant squad was not moved."
|
||||
return
|
||||
|
||||
if move_units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
|
||||
coordinate_label.text = "Ordering %d Peasants to move to q:%d r:%d..." % [selected_peasant_count, to_q, to_r]
|
||||
var error: int = move_units_request.request(
|
||||
LOCATIONS_BASE_URL + "/api/units/move",
|
||||
PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer %s" % access_token
|
||||
]),
|
||||
HTTPClient.METHOD_POST,
|
||||
JSON.stringify({
|
||||
"unitType": "Peasant",
|
||||
"fromQ": selected_peasant_q,
|
||||
"fromR": selected_peasant_r,
|
||||
"toQ": to_q,
|
||||
"toR": to_r
|
||||
})
|
||||
)
|
||||
if error != OK:
|
||||
coordinate_label.text = "Could not contact the unit move service."
|
||||
|
||||
|
||||
func _on_move_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 = "Archer move request failed."
|
||||
coordinate_label.text = "Unit move request failed."
|
||||
return
|
||||
|
||||
if response_code < 200 or response_code >= 300:
|
||||
coordinate_label.text = "Archer move failed: %s" % text
|
||||
coordinate_label.text = "Unit move failed: %s" % text
|
||||
_refresh_player_state()
|
||||
return
|
||||
|
||||
_clear_selected_archers()
|
||||
coordinate_label.text = "Archer squad movement ordered. It will arrive on the next tick."
|
||||
coordinate_label.text = "Unit movement ordered."
|
||||
_load_visible_locations()
|
||||
_load_units()
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ func _draw() -> void:
|
||||
for tile in sorted_tiles:
|
||||
_draw_tile(tile)
|
||||
|
||||
_draw_movement_indicators()
|
||||
|
||||
|
||||
func _draw_grid_lines() -> void:
|
||||
var line_color: Color = Color(0.12, 0.17, 0.17, 0.35)
|
||||
for x in range(-8, 9):
|
||||
@@ -243,6 +246,30 @@ func _draw_control_layer(control_tiles: Array, control: String, layer_scale: flo
|
||||
)
|
||||
|
||||
|
||||
func _draw_movement_indicators() -> void:
|
||||
for unit in unit_instances:
|
||||
var status := str(unit.get("status", ""))
|
||||
if status != "Moving" and status != "MovingToWork":
|
||||
continue
|
||||
if unit.get("destinationQ", null) == null or unit.get("destinationR", null) == null:
|
||||
continue
|
||||
|
||||
var from_q := int(unit.get("q", 0))
|
||||
var from_r := int(unit.get("r", 0))
|
||||
var to_q := int(unit.get("destinationQ", 0))
|
||||
var to_r := int(unit.get("destinationR", 0))
|
||||
var from_center := _tile_center(from_q, from_r)
|
||||
var to_center := _tile_center(to_q, to_r)
|
||||
var target_tile: Dictionary = _tile_data(to_q, to_r)
|
||||
var target_height := float(target_tile.get("height", _tile_height(to_q, to_r)))
|
||||
var marker_color := Color(0.98, 0.82, 0.28, 0.92) if str(unit.get("unitType", "")) == "Peasant" else Color(0.38, 0.86, 1.0, 0.92)
|
||||
|
||||
draw_line(from_center, to_center, Color(marker_color.r, marker_color.g, marker_color.b, 0.72), 3.0)
|
||||
draw_circle(from_center, 4.0, Color(marker_color.r, marker_color.g, marker_color.b, 0.88))
|
||||
draw_polyline(_closed_points(_hex_points(to_center, target_height + 3.0)), marker_color, 4.0)
|
||||
draw_circle(to_center, 7.0, Color(marker_color.r, marker_color.g, marker_color.b, 0.76))
|
||||
|
||||
|
||||
func _update_hover(mouse_position: Vector2) -> void:
|
||||
var closest := _closest_tile_at(mouse_position)
|
||||
|
||||
@@ -695,6 +722,14 @@ func get_peasant_id_at(q: int, r: int) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
func get_peasant_count_at(q: int, r: int) -> int:
|
||||
return _idle_peasant_count_at(q, r)
|
||||
|
||||
|
||||
func is_owned_tile(q: int, r: int) -> bool:
|
||||
return owned_tiles.has(_tile_key(q, r))
|
||||
|
||||
|
||||
func is_visible_non_rival_tile(q: int, r: int) -> bool:
|
||||
var key := _tile_key(q, r)
|
||||
return visible_tiles.has(key) and not rival_tiles.has(key)
|
||||
@@ -829,6 +864,16 @@ func _peasant_count_at(q: int, r: int) -> int:
|
||||
return count
|
||||
|
||||
|
||||
func _idle_peasant_count_at(q: int, r: int) -> int:
|
||||
var count := 0
|
||||
for unit in unit_instances:
|
||||
if str(unit.get("unitType", "")) != "Peasant" or str(unit.get("status", "")) != "Idle":
|
||||
continue
|
||||
if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _owner_username(location: Dictionary) -> String:
|
||||
var owner_value: Variant = location.get("ownerUsername", "")
|
||||
if owner_value == null:
|
||||
|
||||
@@ -9,6 +9,7 @@ public class TrainUnitRequest
|
||||
|
||||
public class MoveUnitSquadRequest
|
||||
{
|
||||
public string? UnitId { get; set; }
|
||||
public string UnitType { get; set; } = "Archer";
|
||||
public int FromQ { get; set; }
|
||||
public int FromR { get; set; }
|
||||
|
||||
@@ -177,9 +177,9 @@ public class UnitTrainingService
|
||||
await RefillMovementAsync(username);
|
||||
|
||||
var normalizedUnitType = NormalizeUnitType(request.UnitType);
|
||||
if (normalizedUnitType != ArcherUnitType)
|
||||
if (normalizedUnitType != ArcherUnitType && normalizedUnitType != PeasantUnitType)
|
||||
{
|
||||
throw new ArgumentException("Only Archer squads can be moved right now.", nameof(request.UnitType));
|
||||
throw new ArgumentException("Only Archers and Peasants can be moved right now.", nameof(request.UnitType));
|
||||
}
|
||||
|
||||
if (request.FromQ == request.ToQ && request.FromR == request.ToR)
|
||||
@@ -187,24 +187,38 @@ public class UnitTrainingService
|
||||
throw new InvalidOperationException("Select a different destination tile.");
|
||||
}
|
||||
|
||||
if (HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR) != 1)
|
||||
var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR);
|
||||
if (normalizedUnitType == ArcherUnitType && movementDistance != 1)
|
||||
{
|
||||
throw new InvalidOperationException("Archers can only move to adjacent tiles.");
|
||||
}
|
||||
|
||||
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
|
||||
if (normalizedUnitType == ArcherUnitType && !await CanMoveToAsync(username, request.ToQ, request.ToR))
|
||||
{
|
||||
throw new InvalidOperationException("Archers can only move to visible tiles.");
|
||||
}
|
||||
|
||||
if (normalizedUnitType == PeasantUnitType && !await CanPeasantMoveToAsync(username, request.ToQ, request.ToR))
|
||||
{
|
||||
throw new InvalidOperationException("Peasants can only move within your controlled territory.");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var units = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Status == "Idle" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR)
|
||||
.ToListAsync();
|
||||
var unitFilter = Builders<UnitInstance>.Filter.And(
|
||||
Builders<UnitInstance>.Filter.Eq(x => x.Username, username),
|
||||
Builders<UnitInstance>.Filter.Eq(x => x.UnitType, normalizedUnitType),
|
||||
Builders<UnitInstance>.Filter.Eq(x => x.Status, "Idle"),
|
||||
Builders<UnitInstance>.Filter.Eq(x => x.Q, request.FromQ),
|
||||
Builders<UnitInstance>.Filter.Eq(x => x.R, request.FromR));
|
||||
if (normalizedUnitType == PeasantUnitType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(request.UnitId))
|
||||
{
|
||||
unitFilter &= Builders<UnitInstance>.Filter.Eq(x => x.Id, request.UnitId);
|
||||
}
|
||||
}
|
||||
|
||||
var units = await _units.Find(unitFilter).ToListAsync();
|
||||
|
||||
if (units.Count == 0)
|
||||
{
|
||||
@@ -216,13 +230,9 @@ public class UnitTrainingService
|
||||
throw new InvalidOperationException("That squad has no movement remaining this tick.");
|
||||
}
|
||||
|
||||
var movementCompletesAt = now.AddMinutes(1);
|
||||
var movementCompletesAt = now.AddMinutes(Math.Max(1, movementDistance));
|
||||
await _units.UpdateManyAsync(
|
||||
x => x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Status == "Idle" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR,
|
||||
unitFilter,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Status, "Moving")
|
||||
.Inc(x => x.MovementRemaining, -1)
|
||||
@@ -234,7 +244,7 @@ public class UnitTrainingService
|
||||
|
||||
return await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.UnitType == normalizedUnitType &&
|
||||
x.Status == "Moving" &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR &&
|
||||
@@ -282,11 +292,34 @@ public class UnitTrainingService
|
||||
continue;
|
||||
}
|
||||
|
||||
var destinationQ = unit.DestinationQ.Value;
|
||||
var destinationR = unit.DestinationR.Value;
|
||||
var currentQ = unit.Q;
|
||||
var currentR = unit.R;
|
||||
var nextTick = unit.MovementCompletesAtUtc!.Value;
|
||||
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||
{
|
||||
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
|
||||
nextTick = nextTick.AddMinutes(1);
|
||||
}
|
||||
|
||||
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||
{
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, currentQ)
|
||||
.Set(x => x.R, currentR)
|
||||
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
continue;
|
||||
}
|
||||
|
||||
await _units.UpdateOneAsync(
|
||||
x => x.Id == unit.Id,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, unit.DestinationQ.Value)
|
||||
.Set(x => x.R, unit.DestinationR.Value)
|
||||
.Set(x => x.Q, destinationQ)
|
||||
.Set(x => x.R, destinationR)
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.MovementPerTick, movementPerTick)
|
||||
.Set(x => x.MovementRemaining, movementPerTick)
|
||||
@@ -299,7 +332,7 @@ public class UnitTrainingService
|
||||
|
||||
if (unit.UnitType == ArcherUnitType)
|
||||
{
|
||||
await ClaimTargetIfEmptyAsync(username, unit.DestinationQ.Value, unit.DestinationR.Value, now);
|
||||
await ClaimTargetIfEmptyAsync(username, destinationQ, destinationR, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +396,15 @@ public class UnitTrainingService
|
||||
return playerUnits.Any(unit => HexDistance(unit.Q, unit.R, q, r) <= 1);
|
||||
}
|
||||
|
||||
private async Task<bool> CanPeasantMoveToAsync(string username, int q, int r)
|
||||
{
|
||||
return await _locations.Find(x =>
|
||||
x.OwnerUsername == username &&
|
||||
x.Q == q &&
|
||||
x.R == r)
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
private async Task ClaimTargetIfEmptyAsync(string username, int q, int r, DateTime now)
|
||||
{
|
||||
var hasOtherUnits = await _units.Find(x => x.Username != username && x.Q == q && x.R == r).AnyAsync();
|
||||
@@ -428,6 +470,25 @@ public class UnitTrainingService
|
||||
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
|
||||
}
|
||||
|
||||
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
||||
{
|
||||
return NeighborCoordinates(fromQ, fromR)
|
||||
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
||||
.ThenBy(x => x.q)
|
||||
.ThenBy(x => x.r)
|
||||
.First();
|
||||
}
|
||||
|
||||
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
|
||||
{
|
||||
yield return (q + 1, r);
|
||||
yield return (q + 1, r - 1);
|
||||
yield return (q, r - 1);
|
||||
yield return (q - 1, r);
|
||||
yield return (q - 1, r + 1);
|
||||
yield return (q, r + 1);
|
||||
}
|
||||
|
||||
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
|
||||
Reference in New Issue
Block a user