territory claiming

This commit is contained in:
hz
2026-05-18 15:40:15 -05:00
parent 37a46a7b36
commit 3167558e04
5 changed files with 127 additions and 4 deletions
@@ -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)
{ {
+3 -2
View File
@@ -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;