territory claiming
This commit is contained in:
@@ -13,6 +13,7 @@ var visible_request: HTTPRequest
|
|||||||
var resources_request: HTTPRequest
|
var resources_request: HTTPRequest
|
||||||
var units_request: HTTPRequest
|
var units_request: HTTPRequest
|
||||||
var train_unit_request: HTTPRequest
|
var train_unit_request: HTTPRequest
|
||||||
|
var claim_request: HTTPRequest
|
||||||
var resources_timer: Timer
|
var resources_timer: Timer
|
||||||
var session: Dictionary = {}
|
var session: Dictionary = {}
|
||||||
var escape_overlay: Control
|
var escape_overlay: Control
|
||||||
@@ -28,6 +29,11 @@ var training_label: Label
|
|||||||
var training_bar: ProgressBar
|
var training_bar: ProgressBar
|
||||||
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 claim_button: Button
|
||||||
|
var selected_q := 0
|
||||||
|
var selected_r := 0
|
||||||
|
var selected_can_claim := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -206,6 +212,11 @@ func _build_ui() -> void:
|
|||||||
train_button.pressed.connect(_train_scout)
|
train_button.pressed.connect(_train_scout)
|
||||||
bottom_content.add_child(train_button)
|
bottom_content.add_child(train_button)
|
||||||
|
|
||||||
|
claim_button = _small_button("Claim 1 Scout")
|
||||||
|
claim_button.disabled = true
|
||||||
|
claim_button.pressed.connect(_claim_selected_tile)
|
||||||
|
bottom_content.add_child(claim_button)
|
||||||
|
|
||||||
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)
|
||||||
add_child(http_request)
|
add_child(http_request)
|
||||||
@@ -226,6 +237,10 @@ func _build_ui() -> void:
|
|||||||
train_unit_request.request_completed.connect(_on_train_unit_request_completed)
|
train_unit_request.request_completed.connect(_on_train_unit_request_completed)
|
||||||
add_child(train_unit_request)
|
add_child(train_unit_request)
|
||||||
|
|
||||||
|
claim_request = HTTPRequest.new()
|
||||||
|
claim_request.request_completed.connect(_on_claim_request_completed)
|
||||||
|
add_child(claim_request)
|
||||||
|
|
||||||
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
|
||||||
@@ -241,6 +256,11 @@ func _on_tile_hovered(q: int, r: int, terrain: String) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
||||||
|
selected_q = q
|
||||||
|
selected_r = r
|
||||||
|
selected_can_claim = terrain.contains("Unclaimed")
|
||||||
|
if claim_button:
|
||||||
|
claim_button.disabled = not selected_can_claim
|
||||||
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]
|
||||||
|
|
||||||
@@ -441,6 +461,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
|||||||
if training.is_empty():
|
if training.is_empty():
|
||||||
training_started_unix = 0.0
|
training_started_unix = 0.0
|
||||||
training_completes_unix = 0.0
|
training_completes_unix = 0.0
|
||||||
|
training_completion_refresh_requested = false
|
||||||
_update_training_bar()
|
_update_training_bar()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -450,6 +471,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
|||||||
var active_training: Dictionary = training[0]
|
var active_training: Dictionary = training[0]
|
||||||
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
|
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
|
||||||
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
|
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
|
||||||
|
training_completion_refresh_requested = false
|
||||||
_update_training_bar()
|
_update_training_bar()
|
||||||
|
|
||||||
|
|
||||||
@@ -495,11 +517,61 @@ func _on_train_unit_request_completed(result: int, response_code: int, _headers:
|
|||||||
var training: Dictionary = parsed
|
var training: Dictionary = parsed
|
||||||
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
|
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
|
||||||
training_completes_unix = _parse_utc_timestamp(str(training.get("completesAtUtc", "")))
|
training_completes_unix = _parse_utc_timestamp(str(training.get("completesAtUtc", "")))
|
||||||
|
training_completion_refresh_requested = false
|
||||||
coordinate_label.text = "Scout training started. Completion takes 1 minute."
|
coordinate_label.text = "Scout training started. Completion takes 1 minute."
|
||||||
|
|
||||||
_refresh_player_state()
|
_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."
|
||||||
|
return
|
||||||
|
|
||||||
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
|
if access_token.is_empty():
|
||||||
|
coordinate_label.text = "No auth token available. Territory was not claimed."
|
||||||
|
return
|
||||||
|
|
||||||
|
if claim_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||||
|
return
|
||||||
|
|
||||||
|
claim_button.disabled = true
|
||||||
|
coordinate_label.text = "Sending scout to claim q:%d r:%d..." % [selected_q, selected_r]
|
||||||
|
var error: int = claim_request.request(
|
||||||
|
LOCATIONS_BASE_URL + "/api/locations/claim",
|
||||||
|
PackedStringArray([
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"Authorization: Bearer %s" % access_token
|
||||||
|
]),
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
JSON.stringify({ "q": selected_q, "r": selected_r })
|
||||||
|
)
|
||||||
|
if error != OK:
|
||||||
|
claim_button.disabled = false
|
||||||
|
coordinate_label.text = "Could not contact the locations service."
|
||||||
|
|
||||||
|
|
||||||
|
func _on_claim_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 = "Scout claim request failed."
|
||||||
|
claim_button.disabled = not selected_can_claim
|
||||||
|
return
|
||||||
|
|
||||||
|
if response_code < 200 or response_code >= 300:
|
||||||
|
coordinate_label.text = "Scout claim failed: %s" % text
|
||||||
|
claim_button.disabled = not selected_can_claim
|
||||||
|
_refresh_player_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_can_claim = false
|
||||||
|
claim_button.disabled = true
|
||||||
|
coordinate_label.text = "Scout claimed q:%d r:%d. Territory expanded." % [selected_q, selected_r]
|
||||||
|
_load_visible_locations()
|
||||||
|
_refresh_player_state()
|
||||||
|
|
||||||
|
|
||||||
func _update_tick_bar() -> void:
|
func _update_tick_bar() -> void:
|
||||||
if not tick_bar or not tick_label:
|
if not tick_bar or not tick_label:
|
||||||
return
|
return
|
||||||
@@ -536,6 +608,9 @@ func _update_training_bar() -> void:
|
|||||||
|
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
training_label.text = "Training: ready"
|
training_label.text = "Training: ready"
|
||||||
|
if not training_completion_refresh_requested:
|
||||||
|
training_completion_refresh_requested = true
|
||||||
|
_load_units()
|
||||||
else:
|
else:
|
||||||
training_label.text = "Training: %ds" % remaining
|
training_label.text = "Training: %ds" % remaining
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ func _gui_input(event: InputEvent) -> void:
|
|||||||
if not event.pressed and drag_distance < 6.0 and hovered_tile.x != 9999:
|
if not event.pressed and drag_distance < 6.0 and hovered_tile.x != 9999:
|
||||||
selected_tile = hovered_tile
|
selected_tile = hovered_tile
|
||||||
var data: Dictionary = _tile_data(selected_tile.x, selected_tile.y)
|
var data: Dictionary = _tile_data(selected_tile.x, selected_tile.y)
|
||||||
tile_selected.emit(selected_tile.x, selected_tile.y, str(data.get("terrain", "Open")))
|
var key: String = _tile_key(selected_tile.x, selected_tile.y)
|
||||||
|
var label: String = _tile_display_label(key, data) if visible_tiles.has(key) else "Unexplored"
|
||||||
|
tile_selected.emit(selected_tile.x, selected_tile.y, label)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
elif event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
elif event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
||||||
zoom = clampf(zoom + 0.08, 0.65, 1.6)
|
zoom = clampf(zoom + 0.08, 0.65, 1.6)
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ namespace LocationsApi.Controllers;
|
|||||||
public class LocationsController : ControllerBase
|
public class LocationsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly LocationService _locations;
|
private readonly LocationService _locations;
|
||||||
|
private readonly UnitTrainingService _units;
|
||||||
|
|
||||||
public LocationsController(LocationService locations)
|
public LocationsController(LocationService locations, UnitTrainingService units)
|
||||||
{
|
{
|
||||||
_locations = locations;
|
_locations = locations;
|
||||||
|
_units = units;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
@@ -86,6 +88,22 @@ public class LocationsController : ControllerBase
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingLocation = await _locations.GetAsync(request.Q, request.R);
|
||||||
|
if (existingLocation is not null && !string.IsNullOrWhiteSpace(existingLocation.OwnerUsername))
|
||||||
|
{
|
||||||
|
if (string.Equals(existingLocation.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Ok(LocationResponse.FromLocation(existingLocation));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Conflict("Location is already controlled by another player.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!User.IsInRole("SUPER") && !await _units.TryConsumeCompletedScoutAsync(username))
|
||||||
|
{
|
||||||
|
return BadRequest("A completed scout is required to claim unclaimed territory.");
|
||||||
|
}
|
||||||
|
|
||||||
var location = await _locations.ClaimAsync(request.Q, request.R, username);
|
var location = await _locations.ClaimAsync(request.Q, request.R, username);
|
||||||
if (location is null)
|
if (location is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Tracks hex locations for the territory map.
|
|||||||
- `GET /api/locations` List visible locations for USER/SUPER, all known locations for SUPER.
|
- `GET /api/locations` List visible locations for USER/SUPER, all known locations for SUPER.
|
||||||
- `GET /api/locations?minQ=-7&maxQ=7&minR=-7&maxR=7` List a coordinate range, constrained by visibility for USER.
|
- `GET /api/locations?minQ=-7&maxQ=7&minR=-7&maxR=7` List a coordinate range, constrained by visibility for USER.
|
||||||
- `GET /api/locations/{q}/{r}` Fetch one visible location for USER, any known location for SUPER.
|
- `GET /api/locations/{q}/{r}` Fetch one visible location for USER, any known location for SUPER.
|
||||||
- `POST /api/locations/claim` Claim a location for the authenticated user.
|
- `POST /api/locations/claim` Claim visible unclaimed territory for the authenticated user. Normal players spend one completed Scout.
|
||||||
- `GET /api/locations/home` Get the authenticated user's home location.
|
- `GET /api/locations/home` Get the authenticated user's home location.
|
||||||
- `POST /api/locations/home` Assign or return the authenticated user's home location.
|
- `POST /api/locations/home` Assign or return the authenticated user's home location.
|
||||||
- `GET /api/locations/visible` Get locations visible to the authenticated user.
|
- `GET /api/locations/visible` Get locations visible to the authenticated user.
|
||||||
@@ -33,4 +33,5 @@ minute. Generation is applied lazily when the resource record is read.
|
|||||||
|
|
||||||
Unit training is stored server-side in the `UnitTraining` collection. The first
|
Unit training is stored server-side in the `UnitTraining` collection. The first
|
||||||
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
|
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
|
||||||
Finished training is completed lazily when unit state is read.
|
Finished training is completed lazily when unit state is read. Claiming an
|
||||||
|
unclaimed visible location consumes one completed Scout.
|
||||||
|
|||||||
@@ -78,6 +78,33 @@ public class UnitTrainingService
|
|||||||
return training;
|
return training;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
||||||
|
{
|
||||||
|
await CompleteFinishedTrainingAsync(username);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var update = Builders<UnitTraining>.Update
|
||||||
|
.Set(x => x.Status, "Consumed")
|
||||||
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
|
var consumed = await _training.FindOneAndUpdateAsync(
|
||||||
|
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
||||||
|
update,
|
||||||
|
new FindOneAndUpdateOptions<UnitTraining>
|
||||||
|
{
|
||||||
|
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc),
|
||||||
|
ReturnDocument = ReturnDocument.After
|
||||||
|
});
|
||||||
|
|
||||||
|
return consumed is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasCompletedScoutAsync(string username)
|
||||||
|
{
|
||||||
|
await CompleteFinishedTrainingAsync(username);
|
||||||
|
return await _training.Find(x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed").AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CompleteFinishedTrainingAsync(string username)
|
private async Task CompleteFinishedTrainingAsync(string username)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|||||||
Reference in New Issue
Block a user