territory claiming modification

This commit is contained in:
hz
2026-05-20 21:10:19 -05:00
parent f142440aca
commit c89c3a0947
4 changed files with 35 additions and 69 deletions
@@ -677,7 +677,7 @@ func _confirm_scout_claim() -> void:
if pending_move_q == 9999: if pending_move_q == 9999:
return return
if not _can_claim_tile(pending_move_q, pending_move_r): if not _can_claim_tile(pending_move_q, pending_move_r):
coordinate_label.text = "Scouts can only claim visible unclaimed territory adjacent to controlled territory." coordinate_label.text = "Scouts can only claim visible neutral or enemy territory adjacent to controlled territory."
return return
selected_q = pending_move_q selected_q = pending_move_q
selected_r = pending_move_r selected_r = pending_move_r
@@ -804,8 +804,9 @@ func _can_claim_tile(q: int, r: int) -> bool:
if location.is_empty(): if location.is_empty():
return false return false
var username: String = str(session.get("username", ""))
var owner_username: String = _nullable_string(location.get("ownerUsername", "")) var owner_username: String = _nullable_string(location.get("ownerUsername", ""))
return owner_username.is_empty() and grid_view.has_owned_neighbor(q, r) return owner_username != username and grid_view.has_owned_neighbor(q, r)
func _update_action_buttons() -> void: func _update_action_buttons() -> void:
@@ -100,19 +100,22 @@ public class LocationsController : ControllerBase
{ {
return Ok(LocationResponse.FromLocation(existingLocation)); return Ok(LocationResponse.FromLocation(existingLocation));
} }
}
return Conflict("Location is already controlled by another player."); if (!User.IsInRole("SUPER") && !await _locations.CanScoutClaimAsync(username, request.Q, request.R))
{
return BadRequest("Scouts can only claim neutral territory or empty enemy territory without units or buildings.");
} }
if (!User.IsInRole("SUPER") && !await _units.TryConsumeCompletedScoutAsync(username)) if (!User.IsInRole("SUPER") && !await _units.TryConsumeCompletedScoutAsync(username))
{ {
return BadRequest("A completed scout is required to claim unclaimed territory."); return BadRequest("A completed scout is required to claim 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)
{ {
return Conflict("Location is already controlled by another player."); return Conflict("Location cannot be claimed while occupied or built up.");
} }
return Ok(LocationResponse.FromLocation(location)); return Ok(LocationResponse.FromLocation(location));
@@ -145,8 +145,12 @@ public class LocationService
var existing = await GetAsync(q, r); var existing = await GetAsync(q, r);
if (existing is not null) if (existing is not null)
{ {
if (!string.IsNullOrWhiteSpace(existing.OwnerUsername) && if (string.Equals(existing.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
!string.Equals(existing.OwnerUsername, username, StringComparison.OrdinalIgnoreCase)) {
return existing;
}
if (!await CanScoutClaimAsync(username, q, r))
{ {
return null; return null;
} }
@@ -164,6 +168,26 @@ public class LocationService
return await UpsertAsync(new UpsertLocationRequest { Q = q, R = r, Terrain = "Field" }, username); return await UpsertAsync(new UpsertLocationRequest { Q = q, R = r, Terrain = "Field" }, username);
} }
public async Task<bool> CanScoutClaimAsync(string username, int q, int r)
{
var existing = await GetAsync(q, r);
if (existing is not null)
{
if (string.Equals(existing.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(existing.Kind, "Home", StringComparison.OrdinalIgnoreCase) ||
!string.IsNullOrWhiteSpace(existing.BuildingType))
{
return false;
}
}
return !await _units.Find(x => x.Q == q && x.R == r).AnyAsync();
}
public async Task<Location?> BuildAsync(BuildLocationRequest request, string username) public async Task<Location?> BuildAsync(BuildLocationRequest request, string username)
{ {
var buildingType = NormalizeBuildingType(request.BuildingType); var buildingType = NormalizeBuildingType(request.BuildingType);
@@ -375,10 +375,6 @@ public class UnitTrainingService
.Unset(x => x.MovementCompletesAtUtc) .Unset(x => x.MovementCompletesAtUtc)
.Set(x => x.UpdatedAtUtc, now)); .Set(x => x.UpdatedAtUtc, now));
if (unit.UnitType == ArcherUnitType)
{
await ClaimTargetIfEmptyAsync(username, destinationQ, destinationR, now);
}
} }
} }
@@ -503,64 +499,6 @@ public class UnitTrainingService
return step == start ? target : step; return step == start ? target : step;
} }
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();
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
var hasBuilding = target is not null &&
(!string.IsNullOrWhiteSpace(target.BuildingType) ||
string.Equals(target.Kind, "Home", StringComparison.OrdinalIgnoreCase));
if (hasOtherUnits || hasBuilding)
{
return;
}
if (target is null)
{
var location = new Location
{
Q = q,
R = r,
Terrain = "Field",
Kind = "Claimed",
OwnerUsername = username,
Level = 1,
CreatedAtUtc = now,
UpdatedAtUtc = now
};
try
{
await _locations.InsertOneAsync(location);
}
catch (MongoWriteException)
{
}
return;
}
if (string.Equals(target.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
{
return;
}
var capturableFilter = Builders<Location>.Filter.And(
Builders<Location>.Filter.Eq(x => x.Id, target.Id),
Builders<Location>.Filter.Ne(x => x.Kind, "Home"),
Builders<Location>.Filter.Or(
Builders<Location>.Filter.Eq(x => x.BuildingType, null),
Builders<Location>.Filter.Eq(x => x.BuildingType, "")));
await _locations.UpdateOneAsync(
capturableFilter,
Builders<Location>.Update
.Set(x => x.OwnerUsername, username)
.Set(x => x.Kind, "Claimed")
.Set(x => x.UpdatedAtUtc, now));
}
private static int HexDistance(int q1, int r1, int q2, int r2) private static int HexDistance(int q1, int r1, int q2, int r2)
{ {
var s1 = -q1 - r1; var s1 = -q1 - r1;