Movement enhancements

This commit is contained in:
hz
2026-05-20 14:33:12 -05:00
parent dfbe1d63c0
commit 84136ad534
4 changed files with 187 additions and 29 deletions
@@ -9,6 +9,7 @@ public class TrainUnitRequest
public class MoveUnitSquadRequest
{
public string? UnitId { get; set; }
public string UnitType { get; set; } = "Archer";
public int FromQ { get; set; }
public int FromR { get; set; }
@@ -177,9 +177,9 @@ public class UnitTrainingService
await RefillMovementAsync(username);
var normalizedUnitType = NormalizeUnitType(request.UnitType);
if (normalizedUnitType != ArcherUnitType)
if (normalizedUnitType != ArcherUnitType && normalizedUnitType != PeasantUnitType)
{
throw new ArgumentException("Only Archer squads can be moved right now.", nameof(request.UnitType));
throw new ArgumentException("Only Archers and Peasants can be moved right now.", nameof(request.UnitType));
}
if (request.FromQ == request.ToQ && request.FromR == request.ToR)
@@ -187,24 +187,38 @@ public class UnitTrainingService
throw new InvalidOperationException("Select a different destination tile.");
}
if (HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR) != 1)
var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR);
if (normalizedUnitType == ArcherUnitType && movementDistance != 1)
{
throw new InvalidOperationException("Archers can only move to adjacent tiles.");
}
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
if (normalizedUnitType == ArcherUnitType && !await CanMoveToAsync(username, request.ToQ, request.ToR))
{
throw new InvalidOperationException("Archers can only move to visible tiles.");
}
if (normalizedUnitType == PeasantUnitType && !await CanPeasantMoveToAsync(username, request.ToQ, request.ToR))
{
throw new InvalidOperationException("Peasants can only move within your controlled territory.");
}
var now = DateTime.UtcNow;
var units = await _units.Find(x =>
x.Username == username &&
x.UnitType == ArcherUnitType &&
x.Status == "Idle" &&
x.Q == request.FromQ &&
x.R == request.FromR)
.ToListAsync();
var unitFilter = Builders<UnitInstance>.Filter.And(
Builders<UnitInstance>.Filter.Eq(x => x.Username, username),
Builders<UnitInstance>.Filter.Eq(x => x.UnitType, normalizedUnitType),
Builders<UnitInstance>.Filter.Eq(x => x.Status, "Idle"),
Builders<UnitInstance>.Filter.Eq(x => x.Q, request.FromQ),
Builders<UnitInstance>.Filter.Eq(x => x.R, request.FromR));
if (normalizedUnitType == PeasantUnitType)
{
if (!string.IsNullOrWhiteSpace(request.UnitId))
{
unitFilter &= Builders<UnitInstance>.Filter.Eq(x => x.Id, request.UnitId);
}
}
var units = await _units.Find(unitFilter).ToListAsync();
if (units.Count == 0)
{
@@ -216,13 +230,9 @@ public class UnitTrainingService
throw new InvalidOperationException("That squad has no movement remaining this tick.");
}
var movementCompletesAt = now.AddMinutes(1);
var movementCompletesAt = now.AddMinutes(Math.Max(1, movementDistance));
await _units.UpdateManyAsync(
x => x.Username == username &&
x.UnitType == ArcherUnitType &&
x.Status == "Idle" &&
x.Q == request.FromQ &&
x.R == request.FromR,
unitFilter,
Builders<UnitInstance>.Update
.Set(x => x.Status, "Moving")
.Inc(x => x.MovementRemaining, -1)
@@ -234,7 +244,7 @@ public class UnitTrainingService
return await _units.Find(x =>
x.Username == username &&
x.UnitType == ArcherUnitType &&
x.UnitType == normalizedUnitType &&
x.Status == "Moving" &&
x.Q == request.FromQ &&
x.R == request.FromR &&
@@ -282,11 +292,34 @@ public class UnitTrainingService
continue;
}
var destinationQ = unit.DestinationQ.Value;
var destinationR = unit.DestinationR.Value;
var currentQ = unit.Q;
var currentR = unit.R;
var nextTick = unit.MovementCompletesAtUtc!.Value;
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
{
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
nextTick = nextTick.AddMinutes(1);
}
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
{
await _units.UpdateOneAsync(
x => x.Id == unit.Id,
Builders<UnitInstance>.Update
.Set(x => x.Q, currentQ)
.Set(x => x.R, currentR)
.Set(x => x.MovementCompletesAtUtc, nextTick)
.Set(x => x.UpdatedAtUtc, now));
continue;
}
await _units.UpdateOneAsync(
x => x.Id == unit.Id,
Builders<UnitInstance>.Update
.Set(x => x.Q, unit.DestinationQ.Value)
.Set(x => x.R, unit.DestinationR.Value)
.Set(x => x.Q, destinationQ)
.Set(x => x.R, destinationR)
.Set(x => x.Status, "Idle")
.Set(x => x.MovementPerTick, movementPerTick)
.Set(x => x.MovementRemaining, movementPerTick)
@@ -299,7 +332,7 @@ public class UnitTrainingService
if (unit.UnitType == ArcherUnitType)
{
await ClaimTargetIfEmptyAsync(username, unit.DestinationQ.Value, unit.DestinationR.Value, now);
await ClaimTargetIfEmptyAsync(username, destinationQ, destinationR, now);
}
}
}
@@ -363,6 +396,15 @@ public class UnitTrainingService
return playerUnits.Any(unit => HexDistance(unit.Q, unit.R, q, r) <= 1);
}
private async Task<bool> CanPeasantMoveToAsync(string username, int q, int r)
{
return await _locations.Find(x =>
x.OwnerUsername == username &&
x.Q == q &&
x.R == r)
.AnyAsync();
}
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();
@@ -428,6 +470,25 @@ public class UnitTrainingService
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
}
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
{
return NeighborCoordinates(fromQ, fromR)
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
.ThenBy(x => x.q)
.ThenBy(x => x.r)
.First();
}
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
{
yield return (q + 1, r);
yield return (q + 1, r - 1);
yield return (q, r - 1);
yield return (q - 1, r);
yield return (q - 1, r + 1);
yield return (q, r + 1);
}
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
{
await CompleteFinishedTrainingAsync(username);