implemented unit movement
This commit is contained in:
@@ -15,6 +15,7 @@ var visible_request: HTTPRequest
|
||||
var resources_request: HTTPRequest
|
||||
var units_request: HTTPRequest
|
||||
var train_unit_request: HTTPRequest
|
||||
var move_units_request: HTTPRequest
|
||||
var claim_request: HTTPRequest
|
||||
var build_request: HTTPRequest
|
||||
var refresh_request: HTTPRequest
|
||||
@@ -63,6 +64,9 @@ var selected_can_train_archer := false
|
||||
var selected_is_home := false
|
||||
var completed_scout_count := 0
|
||||
var completed_archer_count := 0
|
||||
var selected_archer_q := 9999
|
||||
var selected_archer_r := 9999
|
||||
var selected_archer_count := 0
|
||||
var refresh_prompt_active := false
|
||||
var refresh_request_active := false
|
||||
|
||||
@@ -258,6 +262,10 @@ func _build_ui() -> void:
|
||||
train_unit_request.request_completed.connect(_on_train_unit_request_completed)
|
||||
add_child(train_unit_request)
|
||||
|
||||
move_units_request = HTTPRequest.new()
|
||||
move_units_request.request_completed.connect(_on_move_units_request_completed)
|
||||
add_child(move_units_request)
|
||||
|
||||
claim_request = HTTPRequest.new()
|
||||
claim_request.request_completed.connect(_on_claim_request_completed)
|
||||
add_child(claim_request)
|
||||
@@ -410,6 +418,9 @@ func _on_tile_hovered(q: int, r: int, terrain: String) -> void:
|
||||
|
||||
|
||||
func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
||||
if _handle_unit_selection_or_move(q, r):
|
||||
return
|
||||
|
||||
selected_q = q
|
||||
selected_r = r
|
||||
selected_can_claim = terrain.contains("Unclaimed")
|
||||
@@ -423,6 +434,27 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
||||
coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain]
|
||||
|
||||
|
||||
func _handle_unit_selection_or_move(q: int, r: int) -> bool:
|
||||
if not grid_view:
|
||||
return false
|
||||
|
||||
var archer_count: int = grid_view.get_archer_count_at(q, r)
|
||||
if archer_count > 0:
|
||||
selected_archer_q = q
|
||||
selected_archer_r = r
|
||||
selected_archer_count = archer_count
|
||||
grid_view.set_selected_unit_tile(q, r)
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Selected Archer squad: %d at q:%d r:%d. Select a visible non-rival tile to move." % [archer_count, q, r]
|
||||
return true
|
||||
|
||||
if selected_archer_count > 0:
|
||||
_move_selected_archers(q, r)
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _can_build_log_camp_on_selected_tile() -> bool:
|
||||
return _can_build_on_selected_tile("Forest")
|
||||
|
||||
@@ -481,6 +513,14 @@ func _update_action_buttons() -> void:
|
||||
build_barracks_button.disabled = false
|
||||
|
||||
|
||||
func _clear_selected_archers() -> void:
|
||||
selected_archer_q = 9999
|
||||
selected_archer_r = 9999
|
||||
selected_archer_count = 0
|
||||
if grid_view:
|
||||
grid_view.clear_selected_unit_tile()
|
||||
|
||||
|
||||
func _selected_tile_is_home() -> bool:
|
||||
if not grid_view:
|
||||
return false
|
||||
@@ -915,6 +955,60 @@ func _on_train_unit_request_completed(result: int, response_code: int, _headers:
|
||||
_refresh_player_state()
|
||||
|
||||
|
||||
func _move_selected_archers(to_q: int, to_r: int) -> void:
|
||||
if selected_archer_count <= 0:
|
||||
return
|
||||
if to_q == selected_archer_q and to_r == selected_archer_r:
|
||||
coordinate_label.text = "Archer squad is already on that tile."
|
||||
return
|
||||
if not grid_view.is_visible_non_rival_tile(to_q, to_r):
|
||||
coordinate_label.text = "Archers can only move to visible non-rival tiles."
|
||||
return
|
||||
|
||||
var access_token: String = str(session.get("accessToken", ""))
|
||||
if access_token.is_empty():
|
||||
coordinate_label.text = "No auth token available. Archer squad was not moved."
|
||||
return
|
||||
|
||||
if move_units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
|
||||
coordinate_label.text = "Moving %d Archers to q:%d r:%d..." % [selected_archer_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": "Archer",
|
||||
"fromQ": selected_archer_q,
|
||||
"fromR": selected_archer_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."
|
||||
return
|
||||
|
||||
if response_code < 200 or response_code >= 300:
|
||||
coordinate_label.text = "Archer move failed: %s" % text
|
||||
_refresh_player_state()
|
||||
return
|
||||
|
||||
_clear_selected_archers()
|
||||
coordinate_label.text = "Archer squad moved."
|
||||
_refresh_player_state()
|
||||
|
||||
|
||||
func _claim_selected_tile() -> void:
|
||||
if not selected_can_claim:
|
||||
coordinate_label.text = "Select a visible unclaimed tile to send a scout."
|
||||
|
||||
@@ -17,6 +17,7 @@ var drag_distance: float = 0.0
|
||||
var last_mouse_position: Vector2 = Vector2.ZERO
|
||||
var hovered_tile: Vector2i = Vector2i(9999, 9999)
|
||||
var selected_tile: Vector2i = Vector2i(9999, 9999)
|
||||
var selected_unit_tile: Vector2i = Vector2i(9999, 9999)
|
||||
var home_tile: Vector2i = Vector2i(9999, 9999)
|
||||
var tiles: Array[Dictionary] = []
|
||||
var visible_tiles: Dictionary = {}
|
||||
@@ -157,6 +158,8 @@ func _draw_tile(tile: Dictionary) -> void:
|
||||
|
||||
if not tile_visible:
|
||||
draw_polyline(_closed_points(points), Color(0.11, 0.15, 0.15, 0.75), 1.0)
|
||||
elif coord == selected_unit_tile:
|
||||
draw_polyline(_closed_points(points), Color(0.90, 0.68, 0.28), 5.0)
|
||||
elif is_selected:
|
||||
draw_polyline(_closed_points(points), Color(0.95, 0.76, 0.28), 4.0)
|
||||
elif coord == home_tile:
|
||||
@@ -453,12 +456,20 @@ func _draw_archer_unit_marker(center: Vector2, height: float, count: int) -> voi
|
||||
draw_line(marker_center + Vector2(18.0, -1.0) * marker_scale, marker_center + Vector2(14.0, -4.0) * marker_scale, arrow_color, 1.2 * marker_scale)
|
||||
draw_line(marker_center + Vector2(18.0, -1.0) * marker_scale, marker_center + Vector2(14.0, 2.0) * marker_scale, arrow_color, 1.2 * marker_scale)
|
||||
|
||||
if count > 1:
|
||||
var badge_center: Vector2 = marker_center + Vector2(10.0, 9.0) * marker_scale
|
||||
draw_circle(badge_center, 6.0 * marker_scale, badge_color)
|
||||
draw_arc(badge_center, 6.0 * marker_scale, 0.0, TAU, 18, badge_border, 1.4 * marker_scale)
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font, badge_center + Vector2(-3.5, 3.5) * marker_scale, str(min(count, 9)), HORIZONTAL_ALIGNMENT_LEFT, -1, 9 * marker_scale, Color(0.93, 0.86, 0.58))
|
||||
var count_text := str(count)
|
||||
var font := ThemeDB.fallback_font
|
||||
var badge_font_size := int(round(9.0 * marker_scale))
|
||||
var text_size := font.get_string_size(count_text, HORIZONTAL_ALIGNMENT_LEFT, -1, badge_font_size)
|
||||
var badge_size := Vector2(max(14.0 * marker_scale, text_size.x + 7.0 * marker_scale), 12.0 * marker_scale)
|
||||
var badge_rect := Rect2(marker_center + Vector2(5.0, 3.0) * marker_scale, badge_size)
|
||||
draw_rect(badge_rect, badge_color, true)
|
||||
draw_polyline(_closed_points(PackedVector2Array([
|
||||
badge_rect.position,
|
||||
badge_rect.position + Vector2(badge_rect.size.x, 0),
|
||||
badge_rect.position + badge_rect.size,
|
||||
badge_rect.position + Vector2(0, badge_rect.size.y)
|
||||
])), badge_border, 1.2 * marker_scale)
|
||||
draw_string(font, badge_rect.position + Vector2(3.5, 9.0) * marker_scale, count_text, HORIZONTAL_ALIGNMENT_LEFT, -1, badge_font_size, Color(0.93, 0.86, 0.58))
|
||||
|
||||
|
||||
func _tile_data(q: int, r: int) -> Dictionary:
|
||||
@@ -531,6 +542,25 @@ func set_unit_instances(units: Array) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_selected_unit_tile(q: int, r: int) -> void:
|
||||
selected_unit_tile = Vector2i(q, r)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func clear_selected_unit_tile() -> void:
|
||||
selected_unit_tile = Vector2i(9999, 9999)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_archer_count_at(q: int, r: int) -> int:
|
||||
return _archer_count_at(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)
|
||||
|
||||
|
||||
func _tile_key(q: int, r: int) -> String:
|
||||
return "%d,%d" % [q, r]
|
||||
|
||||
|
||||
@@ -59,4 +59,34 @@ public class UnitsController : ControllerBase
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
[HttpPost("move")]
|
||||
public async Task<ActionResult<List<UnitInstanceResponse>>> Move([FromBody] MoveUnitSquadRequest request)
|
||||
{
|
||||
var username = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var units = await _units.MoveSquadAsync(username, request);
|
||||
if (units.Count == 0)
|
||||
{
|
||||
return BadRequest(new { error = "No matching units are available to move." });
|
||||
}
|
||||
|
||||
return Ok(units.Select(UnitInstanceResponse.FromUnit).ToList());
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ public class TrainUnitRequest
|
||||
public int? R { get; set; }
|
||||
}
|
||||
|
||||
public class MoveUnitSquadRequest
|
||||
{
|
||||
public string UnitType { get; set; } = "Archer";
|
||||
public int FromQ { get; set; }
|
||||
public int FromR { get; set; }
|
||||
public int ToQ { get; set; }
|
||||
public int ToR { get; set; }
|
||||
}
|
||||
|
||||
public class UnitTrainingResponse
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
@@ -17,6 +17,7 @@ Tracks hex locations for the territory map.
|
||||
- `GET /api/resources/me` Get the authenticated player's resources.
|
||||
- `GET /api/units/me` Get completed units and active training for the authenticated player.
|
||||
- `POST /api/units/train` Spend gold and start training a unit.
|
||||
- `POST /api/units/move` Move a persisted unit squad to a visible non-rival tile.
|
||||
|
||||
`claim` and `put` require the same JWT issued by AuthApi.
|
||||
|
||||
@@ -41,6 +42,10 @@ training is completed lazily when unit state is read. Claiming an unclaimed
|
||||
visible location consumes one completed Scout by deleting that Scout's training
|
||||
record.
|
||||
|
||||
Completed Archers are stored as persisted unit instances in the `Units`
|
||||
collection with their current `q`/`r` location. Archer squads can move to
|
||||
visible tiles that are unclaimed or controlled by the owning player.
|
||||
|
||||
## Buildings
|
||||
|
||||
Buildings are stored on location records. `LogCamp` can be built on controlled
|
||||
|
||||
@@ -18,6 +18,7 @@ public class UnitTrainingService
|
||||
private readonly int _archerWoodCost;
|
||||
private readonly int _defaultScoutTrainingSeconds;
|
||||
private readonly int _defaultArcherTrainingSeconds;
|
||||
private readonly int _visibilityDepth;
|
||||
|
||||
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public class UnitTrainingService
|
||||
_defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds)
|
||||
? Math.Max(1, archerTrainingSeconds)
|
||||
: 60;
|
||||
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
|
||||
|
||||
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
||||
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
|
||||
@@ -138,6 +140,56 @@ public class UnitTrainingService
|
||||
return training;
|
||||
}
|
||||
|
||||
public async Task<List<UnitInstance>> MoveSquadAsync(string username, MoveUnitSquadRequest request)
|
||||
{
|
||||
var normalizedUnitType = NormalizeUnitType(request.UnitType);
|
||||
if (normalizedUnitType != ArcherUnitType)
|
||||
{
|
||||
throw new ArgumentException("Only Archer squads can be moved right now.", nameof(request.UnitType));
|
||||
}
|
||||
|
||||
if (request.FromQ == request.ToQ && request.FromR == request.ToR)
|
||||
{
|
||||
throw new InvalidOperationException("Select a different destination tile.");
|
||||
}
|
||||
|
||||
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
|
||||
{
|
||||
throw new InvalidOperationException("Archers can only move to visible non-rival tiles.");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var units = await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR)
|
||||
.ToListAsync();
|
||||
|
||||
if (units.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
await _units.UpdateManyAsync(
|
||||
x => x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Q == request.FromQ &&
|
||||
x.R == request.FromR,
|
||||
Builders<UnitInstance>.Update
|
||||
.Set(x => x.Q, request.ToQ)
|
||||
.Set(x => x.R, request.ToR)
|
||||
.Set(x => x.Status, "Idle")
|
||||
.Set(x => x.UpdatedAtUtc, now));
|
||||
|
||||
return await _units.Find(x =>
|
||||
x.Username == username &&
|
||||
x.UnitType == ArcherUnitType &&
|
||||
x.Q == request.ToQ &&
|
||||
x.R == request.ToR)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<Location?> GetBuiltBarracksAsync(string username, int q, int r)
|
||||
{
|
||||
await CompleteFinishedBuildingsAsync(username);
|
||||
@@ -166,6 +218,27 @@ public class UnitTrainingService
|
||||
update);
|
||||
}
|
||||
|
||||
private async Task<bool> CanMoveToAsync(string username, int q, int r)
|
||||
{
|
||||
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||
if (!ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
|
||||
return target is null ||
|
||||
string.IsNullOrWhiteSpace(target.OwnerUsername) ||
|
||||
string.Equals(target.OwnerUsername, username, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static int HexDistance(int q1, int r1, int q2, int r2)
|
||||
{
|
||||
var s1 = -q1 - r1;
|
||||
var s2 = -q2 - r2;
|
||||
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
|
||||
}
|
||||
|
||||
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
|
||||
Reference in New Issue
Block a user