territory claiming
This commit is contained in:
@@ -13,6 +13,7 @@ var visible_request: HTTPRequest
|
||||
var resources_request: HTTPRequest
|
||||
var units_request: HTTPRequest
|
||||
var train_unit_request: HTTPRequest
|
||||
var claim_request: HTTPRequest
|
||||
var resources_timer: Timer
|
||||
var session: Dictionary = {}
|
||||
var escape_overlay: Control
|
||||
@@ -28,6 +29,11 @@ var training_label: Label
|
||||
var training_bar: ProgressBar
|
||||
var training_started_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:
|
||||
@@ -206,6 +212,11 @@ func _build_ui() -> void:
|
||||
train_button.pressed.connect(_train_scout)
|
||||
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.request_completed.connect(_on_home_request_completed)
|
||||
add_child(http_request)
|
||||
@@ -226,6 +237,10 @@ func _build_ui() -> void:
|
||||
train_unit_request.request_completed.connect(_on_train_unit_request_completed)
|
||||
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.wait_time = RESOURCE_REFRESH_SECONDS
|
||||
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:
|
||||
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:
|
||||
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():
|
||||
training_started_unix = 0.0
|
||||
training_completes_unix = 0.0
|
||||
training_completion_refresh_requested = false
|
||||
_update_training_bar()
|
||||
return
|
||||
|
||||
@@ -450,6 +471,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
||||
var active_training: Dictionary = training[0]
|
||||
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
|
||||
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
|
||||
training_completion_refresh_requested = false
|
||||
_update_training_bar()
|
||||
|
||||
|
||||
@@ -495,11 +517,61 @@ func _on_train_unit_request_completed(result: int, response_code: int, _headers:
|
||||
var training: Dictionary = parsed
|
||||
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
|
||||
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."
|
||||
|
||||
_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:
|
||||
if not tick_bar or not tick_label:
|
||||
return
|
||||
@@ -536,6 +608,9 @@ func _update_training_bar() -> void:
|
||||
|
||||
if remaining <= 0:
|
||||
training_label.text = "Training: ready"
|
||||
if not training_completion_refresh_requested:
|
||||
training_completion_refresh_requested = true
|
||||
_load_units()
|
||||
else:
|
||||
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:
|
||||
selected_tile = hovered_tile
|
||||
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()
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
||||
zoom = clampf(zoom + 0.08, 0.65, 1.6)
|
||||
|
||||
@@ -11,10 +11,12 @@ namespace LocationsApi.Controllers;
|
||||
public class LocationsController : ControllerBase
|
||||
{
|
||||
private readonly LocationService _locations;
|
||||
private readonly UnitTrainingService _units;
|
||||
|
||||
public LocationsController(LocationService locations)
|
||||
public LocationsController(LocationService locations, UnitTrainingService units)
|
||||
{
|
||||
_locations = locations;
|
||||
_units = units;
|
||||
}
|
||||
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
@@ -86,6 +88,22 @@ public class LocationsController : ControllerBase
|
||||
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);
|
||||
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?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.
|
||||
- `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.
|
||||
- `POST /api/locations/home` Assign or return the authenticated user's home location.
|
||||
- `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 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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user