adding log camps
This commit is contained in:
@@ -113,6 +113,36 @@ public class LocationsController : ControllerBase
|
||||
return Ok(LocationResponse.FromLocation(location));
|
||||
}
|
||||
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
[HttpPost("build")]
|
||||
public async Task<ActionResult<LocationResponse>> Build([FromBody] BuildLocationRequest request)
|
||||
{
|
||||
var username = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var location = await _locations.BuildAsync(request, username);
|
||||
if (location is null)
|
||||
{
|
||||
return NotFound("Build target must be controlled by the player.");
|
||||
}
|
||||
|
||||
return Ok(LocationResponse.FromLocation(location));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
[HttpGet("home")]
|
||||
public async Task<ActionResult<LocationResponse>> GetHome()
|
||||
|
||||
@@ -15,6 +15,13 @@ public class ClaimLocationRequest
|
||||
public int R { get; set; }
|
||||
}
|
||||
|
||||
public class BuildLocationRequest
|
||||
{
|
||||
public int Q { get; set; }
|
||||
public int R { get; set; }
|
||||
public string BuildingType { get; set; } = "LogCamp";
|
||||
}
|
||||
|
||||
public class LocationResponse
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
@@ -25,6 +32,8 @@ public class LocationResponse
|
||||
public string? OwnerUsername { get; set; }
|
||||
public string? Label { get; set; }
|
||||
public int Level { get; set; }
|
||||
public string? BuildingType { get; set; }
|
||||
public DateTime? BuildingBuiltAtUtc { get; set; }
|
||||
public DateTime? HomeAssignedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
@@ -39,6 +48,8 @@ public class LocationResponse
|
||||
OwnerUsername = location.OwnerUsername,
|
||||
Label = location.Label,
|
||||
Level = location.Level,
|
||||
BuildingType = location.BuildingType,
|
||||
BuildingBuiltAtUtc = location.BuildingBuiltAtUtc,
|
||||
HomeAssignedAtUtc = location.HomeAssignedAtUtc,
|
||||
UpdatedAtUtc = location.UpdatedAtUtc,
|
||||
IsVisible = true
|
||||
|
||||
@@ -16,6 +16,8 @@ public class Location
|
||||
public string? OwnerUsername { get; set; }
|
||||
public string? Label { get; set; }
|
||||
public int Level { get; set; } = 1;
|
||||
public string? BuildingType { get; set; }
|
||||
public DateTime? BuildingBuiltAtUtc { get; set; }
|
||||
public DateTime? HomeAssignedAtUtc { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
@@ -12,7 +12,10 @@ public class PlayerResources
|
||||
public string Username { get; set; } = "";
|
||||
public int Gold { get; set; } = 100;
|
||||
public int GoldPerMinute { get; set; } = 10;
|
||||
public int Wood { get; set; }
|
||||
public int WoodPerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ public class PlayerResourcesResponse
|
||||
public string Username { get; set; } = "";
|
||||
public int Gold { get; set; }
|
||||
public int GoldPerMinute { get; set; }
|
||||
public int Wood { get; set; }
|
||||
public int WoodPerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
||||
public DateTime LastWoodGeneratedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
public static PlayerResourcesResponse FromResources(PlayerResources resources) => new()
|
||||
@@ -13,7 +16,10 @@ public class PlayerResourcesResponse
|
||||
Username = resources.Username,
|
||||
Gold = resources.Gold,
|
||||
GoldPerMinute = resources.GoldPerMinute,
|
||||
Wood = resources.Wood,
|
||||
WoodPerMinute = resources.WoodPerMinute,
|
||||
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
||||
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
|
||||
UpdatedAtUtc = resources.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ Tracks hex locations for the territory map.
|
||||
- `GET /api/locations?minQ=-7&maxQ=7&minR=-7&maxR=7` List a coordinate range, constrained by visibility for USER.
|
||||
- `GET /api/locations/{q}/{r}` Fetch one visible location for USER, any known location for SUPER.
|
||||
- `POST /api/locations/claim` Claim visible unclaimed territory for the authenticated user. Normal players spend one completed Scout.
|
||||
- `POST /api/locations/build` Build on controlled territory.
|
||||
- `GET /api/locations/home` Get the authenticated user's home location.
|
||||
- `POST /api/locations/home` Assign or return the authenticated user's home location.
|
||||
- `GET /api/locations/visible` Get locations visible to the authenticated user.
|
||||
@@ -27,7 +28,8 @@ locations are unowned `Open` records until claimed by a player.
|
||||
|
||||
Player resource state is stored server-side in the `PlayerResources` collection.
|
||||
New players start with 100 gold. A player's headquarters generates 10 gold per
|
||||
minute. Generation is applied lazily when the resource record is read.
|
||||
minute. Log Camps generate wood. Generation is applied lazily when the resource
|
||||
record is read.
|
||||
|
||||
## Units
|
||||
|
||||
@@ -35,3 +37,9 @@ Unit training is stored server-side in the `UnitTraining` collection. The first
|
||||
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
|
||||
Finished training is completed lazily when unit state is read. Claiming an
|
||||
unclaimed visible location consumes one completed Scout.
|
||||
|
||||
## Buildings
|
||||
|
||||
Buildings are stored on location records. The first building type is `LogCamp`,
|
||||
which can be built on controlled Forest territory and adds 5 wood per minute to
|
||||
the owning player's resource generation.
|
||||
|
||||
@@ -10,6 +10,7 @@ public class LocationService
|
||||
private readonly int _homeMinDistance;
|
||||
private readonly int _homeSearchRadius;
|
||||
private readonly int _visibilityDepth;
|
||||
private readonly int _logCampWoodPerMinute;
|
||||
|
||||
public LocationService(IConfiguration cfg, ResourceService resources)
|
||||
{
|
||||
@@ -22,6 +23,9 @@ public class LocationService
|
||||
_homeMinDistance = int.TryParse(cfg["World:HomeMinDistance"], out var minDistance) ? Math.Max(1, minDistance) : 8;
|
||||
_homeSearchRadius = int.TryParse(cfg["World:HomeSearchRadius"], out var searchRadius) ? Math.Max(_homeMinDistance, searchRadius) : 80;
|
||||
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
|
||||
_logCampWoodPerMinute = int.TryParse(cfg["Buildings:LogCampWoodPerMinute"], out var logCampWoodPerMinute)
|
||||
? Math.Max(1, logCampWoodPerMinute)
|
||||
: 5;
|
||||
|
||||
var coordinateIndex = Builders<Location>.IndexKeys
|
||||
.Ascending(x => x.Q)
|
||||
@@ -111,6 +115,53 @@ public class LocationService
|
||||
return await UpsertAsync(new UpsertLocationRequest { Q = q, R = r, Terrain = "Field" }, username);
|
||||
}
|
||||
|
||||
public async Task<Location?> BuildAsync(BuildLocationRequest request, string username)
|
||||
{
|
||||
if (!string.Equals(request.BuildingType, "LogCamp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Unsupported building type.", nameof(request.BuildingType));
|
||||
}
|
||||
|
||||
var location = await GetAsync(request.Q, request.R);
|
||||
if (location is null ||
|
||||
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(location.BuildingType))
|
||||
{
|
||||
throw new InvalidOperationException("This location already has a building.");
|
||||
}
|
||||
|
||||
if (!string.Equals(location.Terrain, "Forest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Log Camps can only be built on Forest territory.");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var update = Builders<Location>.Update
|
||||
.Set(x => x.BuildingType, "LogCamp")
|
||||
.Set(x => x.BuildingBuiltAtUtc, now)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
var updated = await _locations.FindOneAndUpdateAsync(
|
||||
x => x.Id == location.Id && x.OwnerUsername == username && x.BuildingType == null,
|
||||
update,
|
||||
new FindOneAndUpdateOptions<Location>
|
||||
{
|
||||
ReturnDocument = ReturnDocument.After
|
||||
});
|
||||
|
||||
if (updated is null)
|
||||
{
|
||||
throw new InvalidOperationException("This location already has a building.");
|
||||
}
|
||||
|
||||
await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute);
|
||||
return updated;
|
||||
}
|
||||
|
||||
public async Task<Location?> GetHomeAsync(string username) =>
|
||||
await _locations.Find(x => x.OwnerUsername == username && x.Kind == "Home").FirstOrDefaultAsync();
|
||||
|
||||
|
||||
@@ -32,8 +32,10 @@ public class ResourceService
|
||||
var update = Builders<PlayerResources>.Update
|
||||
.SetOnInsert(x => x.Username, username)
|
||||
.SetOnInsert(x => x.Gold, _startingGold)
|
||||
.SetOnInsert(x => x.Wood, 0)
|
||||
.SetOnInsert(x => x.CreatedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastGoldGeneratedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
|
||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
@@ -78,29 +80,61 @@ public class ResourceService
|
||||
return updated is not null;
|
||||
}
|
||||
|
||||
public async Task AddWoodGenerationAsync(string username, int woodPerMinute)
|
||||
{
|
||||
if (woodPerMinute <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var resources = await EnsureAsync(username);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var update = Builders<PlayerResources>.Update
|
||||
.Inc(x => x.WoodPerMinute, woodPerMinute)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
if (resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000)
|
||||
{
|
||||
update = update.Set(x => x.LastWoodGeneratedAtUtc, now);
|
||||
}
|
||||
|
||||
await _resources.UpdateOneAsync(x => x.Username == username, update);
|
||||
}
|
||||
|
||||
private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources)
|
||||
{
|
||||
if (resources.GoldPerMinute <= 0)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = now - resources.LastGoldGeneratedAtUtc;
|
||||
var elapsedMinutes = (int)Math.Floor(elapsed.TotalMinutes);
|
||||
if (elapsedMinutes <= 0)
|
||||
var goldElapsedMinutes = resources.GoldPerMinute <= 0
|
||||
? 0
|
||||
: (int)Math.Floor((now - resources.LastGoldGeneratedAtUtc).TotalMinutes);
|
||||
var woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000
|
||||
? 0
|
||||
: (int)Math.Floor((now - resources.LastWoodGeneratedAtUtc).TotalMinutes);
|
||||
|
||||
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
|
||||
var generatedGold = elapsedMinutes * resources.GoldPerMinute;
|
||||
var generationTimestamp = resources.LastGoldGeneratedAtUtc.AddMinutes(elapsedMinutes);
|
||||
var update = Builders<PlayerResources>.Update
|
||||
.Inc(x => x.Gold, generatedGold)
|
||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||
.Set(x => x.LastGoldGeneratedAtUtc, generationTimestamp)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
if (goldElapsedMinutes > 0)
|
||||
{
|
||||
update = update
|
||||
.Inc(x => x.Gold, goldElapsedMinutes * resources.GoldPerMinute)
|
||||
.Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes));
|
||||
}
|
||||
|
||||
if (woodElapsedMinutes > 0)
|
||||
{
|
||||
update = update
|
||||
.Inc(x => x.Wood, woodElapsedMinutes * resources.WoodPerMinute)
|
||||
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
|
||||
}
|
||||
|
||||
return await _resources.FindOneAndUpdateAsync(
|
||||
x => x.Username == resources.Username,
|
||||
update,
|
||||
|
||||
@@ -27,5 +27,8 @@
|
||||
"ScoutGoldCost": 50,
|
||||
"ScoutTrainingSeconds": 60
|
||||
},
|
||||
"Buildings": {
|
||||
"LogCampWoodPerMinute": 5
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@ data:
|
||||
Resources__HeadquartersGoldPerMinute: "10"
|
||||
Units__ScoutGoldCost: "50"
|
||||
Units__ScoutTrainingSeconds: "60"
|
||||
Buildings__LogCampWoodPerMinute: "5"
|
||||
|
||||
Reference in New Issue
Block a user