implemented unit movement
This commit is contained in:
@@ -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; } = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user