This commit is contained in:
hz
2026-05-21 02:21:54 -05:00
parent de42f85835
commit 5530a185a0
6 changed files with 216 additions and 52 deletions
@@ -87,6 +87,10 @@ public class LocationsController : ControllerBase
{
return NotFound();
}
if (!User.IsInRole("SUPER"))
{
return BadRequest("Use a Scout move order to claim territory on arrival.");
}
if (!User.IsInRole("SUPER") && !await _locations.IsAdjacentToOwnedAsync(username, request.Q, request.R))
{
@@ -15,6 +15,7 @@ public class MoveUnitSquadRequest
public int FromR { get; set; }
public int ToQ { get; set; }
public int ToR { get; set; }
public bool ClaimOnArrival { get; set; }
}
public class DismissUnitSquadRequest
@@ -75,6 +76,7 @@ public class UnitInstanceResponse
public DateTime LastMovementRefillAtUtc { get; set; }
public int? DestinationQ { get; set; }
public int? DestinationR { get; set; }
public bool ClaimOnArrival { get; set; }
public DateTime? MovementStartedAtUtc { get; set; }
public DateTime? MovementCompletesAtUtc { get; set; }
public DateTime CreatedAtUtc { get; set; }
@@ -93,6 +95,7 @@ public class UnitInstanceResponse
LastMovementRefillAtUtc = unit.LastMovementRefillAtUtc,
DestinationQ = unit.DestinationQ,
DestinationR = unit.DestinationR,
ClaimOnArrival = unit.ClaimOnArrival,
MovementStartedAtUtc = unit.MovementStartedAtUtc,
MovementCompletesAtUtc = unit.MovementCompletesAtUtc,
CreatedAtUtc = unit.CreatedAtUtc,
@@ -19,6 +19,7 @@ public class UnitInstance
public DateTime LastMovementRefillAtUtc { get; set; } = DateTime.UtcNow;
public int? DestinationQ { get; set; }
public int? DestinationR { get; set; }
public bool ClaimOnArrival { get; set; }
public DateTime? MovementStartedAtUtc { get; set; }
public DateTime? MovementCompletesAtUtc { get; set; }
public string? SourceTrainingId { get; set; }
@@ -190,6 +190,10 @@ public class UnitTrainingService
{
throw new ArgumentException("Only Scouts, Archers, and Peasants can be moved right now.", nameof(request.UnitType));
}
if (request.ClaimOnArrival && normalizedUnitType != ScoutUnitType)
{
throw new ArgumentException("Only Scouts can claim territory.", nameof(request.ClaimOnArrival));
}
if (request.FromQ == request.ToQ && request.FromR == request.ToR)
{
@@ -206,6 +210,10 @@ public class UnitTrainingService
{
throw new InvalidOperationException("Peasants can only move within your controlled territory.");
}
if (request.ClaimOnArrival && !await IsAdjacentToOwnedAsync(username, request.ToQ, request.ToR))
{
throw new InvalidOperationException("Scout claims must target territory adjacent to controlled territory.");
}
var now = DateTime.UtcNow;
var unitFilter = Builders<UnitInstance>.Filter.And(
@@ -234,26 +242,24 @@ public class UnitTrainingService
throw new InvalidOperationException("That squad has no movement remaining this tick.");
}
var movingUnitIds = request.ClaimOnArrival
? units.OrderBy(x => x.Id).Take(1).Select(x => x.Id).ToList()
: units.Select(x => x.Id).ToList();
var movementCompletesAt = now.AddMinutes(1);
await _units.UpdateManyAsync(
unitFilter,
Builders<UnitInstance>.Filter.In(x => x.Id, movingUnitIds),
Builders<UnitInstance>.Update
.Set(x => x.Status, "Moving")
.Inc(x => x.MovementRemaining, -1)
.Set(x => x.DestinationQ, request.ToQ)
.Set(x => x.DestinationR, request.ToR)
.Set(x => x.ClaimOnArrival, request.ClaimOnArrival)
.Set(x => x.MovementStartedAtUtc, now)
.Set(x => x.MovementCompletesAtUtc, movementCompletesAt)
.Set(x => x.UpdatedAtUtc, now));
return await _units.Find(x =>
x.Username == username &&
x.UnitType == normalizedUnitType &&
x.Status == "Moving" &&
x.Q == request.FromQ &&
x.R == request.FromR &&
x.DestinationQ == request.ToQ &&
x.DestinationR == request.ToR)
movingUnitIds.Contains(x.Id))
.ToListAsync();
}
@@ -310,6 +316,7 @@ public class UnitTrainingService
.Set(x => x.Status, "Idle")
.Set(x => x.MovementPerTick, movementPerTick)
.Set(x => x.MovementRemaining, movementPerTick)
.Set(x => x.ClaimOnArrival, false)
.Unset(x => x.MovementStartedAtUtc)
.Unset(x => x.MovementCompletesAtUtc)
.Set(x => x.UpdatedAtUtc, now));
@@ -331,6 +338,7 @@ public class UnitTrainingService
.Set(x => x.MovementRemaining, movementPerTick)
.Unset(x => x.DestinationQ)
.Unset(x => x.DestinationR)
.Set(x => x.ClaimOnArrival, false)
.Unset(x => x.MovementStartedAtUtc)
.Unset(x => x.MovementCompletesAtUtc)
.Set(x => x.UpdatedAtUtc, now));
@@ -371,10 +379,16 @@ public class UnitTrainingService
.Set(x => x.LastMovementRefillAtUtc, now)
.Unset(x => x.DestinationQ)
.Unset(x => x.DestinationR)
.Set(x => x.ClaimOnArrival, false)
.Unset(x => x.MovementStartedAtUtc)
.Unset(x => x.MovementCompletesAtUtc)
.Set(x => x.UpdatedAtUtc, now));
if (unit.UnitType == ScoutUnitType && unit.ClaimOnArrival)
{
await TryClaimWithArrivedScoutAsync(unit, destinationQ, destinationR, now);
}
}
}
@@ -409,6 +423,84 @@ public class UnitTrainingService
}
}
private async Task<bool> IsAdjacentToOwnedAsync(string username, int q, int r)
{
var owned = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
return owned.Any(location => HexDistance(location.Q, location.R, q, r) == 1);
}
private async Task TryClaimWithArrivedScoutAsync(UnitInstance scout, int q, int r, DateTime now)
{
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
if (target is not null)
{
if (string.Equals(target.OwnerUsername, scout.Username, StringComparison.OrdinalIgnoreCase) ||
string.Equals(target.Kind, "Home", StringComparison.OrdinalIgnoreCase) ||
!string.IsNullOrWhiteSpace(target.BuildingType))
{
return;
}
}
var hasBlockingUnit = await _units.Find(x =>
x.Id != scout.Id &&
x.Q == q &&
x.R == r)
.AnyAsync();
if (hasBlockingUnit)
{
return;
}
if (!await IsAdjacentToOwnedAsync(scout.Username, q, r))
{
return;
}
var claimed = false;
if (target is null)
{
var location = new Location
{
Q = q,
R = r,
Terrain = "Field",
Kind = "Claimed",
OwnerUsername = scout.Username,
Level = 1,
CreatedAtUtc = now,
UpdatedAtUtc = now
};
try
{
await _locations.InsertOneAsync(location);
claimed = true;
}
catch (MongoWriteException)
{
}
}
else
{
var result = await _locations.UpdateOneAsync(
x => x.Id == target.Id &&
x.Kind != "Home" &&
(x.BuildingType == null || x.BuildingType == "") &&
x.OwnerUsername != scout.Username,
Builders<Location>.Update
.Set(x => x.OwnerUsername, scout.Username)
.Set(x => x.Kind, "Claimed")
.Set(x => x.UpdatedAtUtc, now));
claimed = result.ModifiedCount > 0;
}
if (claimed)
{
await _units.DeleteOneAsync(x => x.Id == scout.Id);
}
}
private async Task CompleteFinishedBuildingsAsync(string username)
{
var now = DateTime.UtcNow;