implemented unit movement

This commit is contained in:
hz
2026-05-19 05:29:51 -05:00
parent b0b270f4d8
commit 14e2c26e74
6 changed files with 247 additions and 6 deletions
@@ -59,4 +59,34 @@ public class UnitsController : ControllerBase
return BadRequest(new { error = ex.Message });
}
}
[Authorize(Roles = "USER,SUPER")]
[HttpPost("move")]
public async Task<ActionResult<List<UnitInstanceResponse>>> Move([FromBody] MoveUnitSquadRequest request)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
try
{
var units = await _units.MoveSquadAsync(username, request);
if (units.Count == 0)
{
return BadRequest(new { error = "No matching units are available to move." });
}
return Ok(units.Select(UnitInstanceResponse.FromUnit).ToList());
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
catch (InvalidOperationException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
@@ -7,6 +7,15 @@ public class TrainUnitRequest
public int? R { get; set; }
}
public class MoveUnitSquadRequest
{
public string UnitType { get; set; } = "Archer";
public int FromQ { get; set; }
public int FromR { get; set; }
public int ToQ { get; set; }
public int ToR { get; set; }
}
public class UnitTrainingResponse
{
public string Id { get; set; } = "";
+5
View File
@@ -17,6 +17,7 @@ Tracks hex locations for the territory map.
- `GET /api/resources/me` Get the authenticated player's resources.
- `GET /api/units/me` Get completed units and active training for the authenticated player.
- `POST /api/units/train` Spend gold and start training a unit.
- `POST /api/units/move` Move a persisted unit squad to a visible non-rival tile.
`claim` and `put` require the same JWT issued by AuthApi.
@@ -41,6 +42,10 @@ training is completed lazily when unit state is read. Claiming an unclaimed
visible location consumes one completed Scout by deleting that Scout's training
record.
Completed Archers are stored as persisted unit instances in the `Units`
collection with their current `q`/`r` location. Archer squads can move to
visible tiles that are unclaimed or controlled by the owning player.
## Buildings
Buildings are stored on location records. `LogCamp` can be built on controlled
@@ -18,6 +18,7 @@ public class UnitTrainingService
private readonly int _archerWoodCost;
private readonly int _defaultScoutTrainingSeconds;
private readonly int _defaultArcherTrainingSeconds;
private readonly int _visibilityDepth;
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
{
@@ -39,6 +40,7 @@ public class UnitTrainingService
_defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds)
? Math.Max(1, archerTrainingSeconds)
: 60;
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
@@ -138,6 +140,56 @@ public class UnitTrainingService
return training;
}
public async Task<List<UnitInstance>> MoveSquadAsync(string username, MoveUnitSquadRequest request)
{
var normalizedUnitType = NormalizeUnitType(request.UnitType);
if (normalizedUnitType != ArcherUnitType)
{
throw new ArgumentException("Only Archer squads can be moved right now.", nameof(request.UnitType));
}
if (request.FromQ == request.ToQ && request.FromR == request.ToR)
{
throw new InvalidOperationException("Select a different destination tile.");
}
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
{
throw new InvalidOperationException("Archers can only move to visible non-rival tiles.");
}
var now = DateTime.UtcNow;
var units = await _units.Find(x =>
x.Username == username &&
x.UnitType == ArcherUnitType &&
x.Q == request.FromQ &&
x.R == request.FromR)
.ToListAsync();
if (units.Count == 0)
{
return [];
}
await _units.UpdateManyAsync(
x => x.Username == username &&
x.UnitType == ArcherUnitType &&
x.Q == request.FromQ &&
x.R == request.FromR,
Builders<UnitInstance>.Update
.Set(x => x.Q, request.ToQ)
.Set(x => x.R, request.ToR)
.Set(x => x.Status, "Idle")
.Set(x => x.UpdatedAtUtc, now));
return await _units.Find(x =>
x.Username == username &&
x.UnitType == ArcherUnitType &&
x.Q == request.ToQ &&
x.R == request.ToR)
.ToListAsync();
}
private async Task<Location?> GetBuiltBarracksAsync(string username, int q, int r)
{
await CompleteFinishedBuildingsAsync(username);
@@ -166,6 +218,27 @@ public class UnitTrainingService
update);
}
private async Task<bool> CanMoveToAsync(string username, int q, int r)
{
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
if (!ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth))
{
return false;
}
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
return target is null ||
string.IsNullOrWhiteSpace(target.OwnerUsername) ||
string.Equals(target.OwnerUsername, username, StringComparison.OrdinalIgnoreCase);
}
private static int HexDistance(int q1, int r1, int q2, int r2)
{
var s1 = -q1 - r1;
var s2 = -q2 - r2;
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
}
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
{
await CompleteFinishedTrainingAsync(username);