adding resources
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using LocationsApi.Models;
|
||||
using LocationsApi.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace LocationsApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ResourcesController : ControllerBase
|
||||
{
|
||||
private readonly ResourceService _resources;
|
||||
|
||||
public ResourcesController(ResourceService resources)
|
||||
{
|
||||
_resources = resources;
|
||||
}
|
||||
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult<PlayerResourcesResponse>> GetMine()
|
||||
{
|
||||
var username = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var resources = await _resources.GetAsync(username);
|
||||
return Ok(PlayerResourcesResponse.FromResources(resources));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace LocationsApi.Models;
|
||||
|
||||
public class PlayerResources
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
public string Username { get; set; } = "";
|
||||
public int Gold { get; set; } = 100;
|
||||
public int GoldPerMinute { get; set; } = 10;
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace LocationsApi.Models;
|
||||
|
||||
public class PlayerResourcesResponse
|
||||
{
|
||||
public string Username { get; set; } = "";
|
||||
public int Gold { get; set; }
|
||||
public int GoldPerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
public static PlayerResourcesResponse FromResources(PlayerResources resources) => new()
|
||||
{
|
||||
Username = resources.Username,
|
||||
Gold = resources.Gold,
|
||||
GoldPerMinute = resources.GoldPerMinute,
|
||||
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
||||
UpdatedAtUtc = resources.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddSingleton<LocationService>();
|
||||
builder.Services.AddSingleton<ResourceService>();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
|
||||
@@ -13,9 +13,16 @@ Tracks hex locations for the territory map.
|
||||
- `GET /api/locations/visible` Get locations visible to the authenticated user.
|
||||
- `GET /api/locations/visible?depth=3` Override visibility depth for a request.
|
||||
- `PUT /api/locations` Upsert location data (SUPER only).
|
||||
- `GET /api/resources/me` Get the authenticated player's resources.
|
||||
|
||||
`claim` and `put` require the same JWT issued by AuthApi.
|
||||
|
||||
When a player gains visibility over previously unseen coordinates, the service
|
||||
generates and persists those locations before returning them. Generated visible
|
||||
locations are unowned `Open` records until claimed by a player.
|
||||
|
||||
## Resources
|
||||
|
||||
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.
|
||||
|
||||
@@ -6,17 +6,19 @@ namespace LocationsApi.Services;
|
||||
public class LocationService
|
||||
{
|
||||
private readonly IMongoCollection<Location> _locations;
|
||||
private readonly ResourceService _resources;
|
||||
private readonly int _homeMinDistance;
|
||||
private readonly int _homeSearchRadius;
|
||||
private readonly int _visibilityDepth;
|
||||
|
||||
public LocationService(IConfiguration cfg)
|
||||
public LocationService(IConfiguration cfg, ResourceService resources)
|
||||
{
|
||||
var connectionString = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||
var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler";
|
||||
var client = new MongoClient(connectionString);
|
||||
var database = client.GetDatabase(databaseName);
|
||||
_locations = database.GetCollection<Location>("Locations");
|
||||
_resources = resources;
|
||||
_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;
|
||||
@@ -117,6 +119,7 @@ public class LocationService
|
||||
var existingHome = await GetHomeAsync(username);
|
||||
if (existingHome is not null)
|
||||
{
|
||||
await _resources.EnsureAsync(username);
|
||||
return existingHome;
|
||||
}
|
||||
|
||||
@@ -138,6 +141,7 @@ public class LocationService
|
||||
var home = await TryCreateHomeAsync(username, candidate.q, candidate.r);
|
||||
if (home is not null)
|
||||
{
|
||||
await _resources.EnsureAsync(username);
|
||||
return home;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using LocationsApi.Models;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace LocationsApi.Services;
|
||||
|
||||
public class ResourceService
|
||||
{
|
||||
private readonly IMongoCollection<PlayerResources> _resources;
|
||||
private readonly int _startingGold;
|
||||
private readonly int _headquartersGoldPerMinute;
|
||||
|
||||
public ResourceService(IConfiguration cfg)
|
||||
{
|
||||
var connectionString = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||
var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler";
|
||||
var client = new MongoClient(connectionString);
|
||||
var database = client.GetDatabase(databaseName);
|
||||
_resources = database.GetCollection<PlayerResources>("PlayerResources");
|
||||
_startingGold = int.TryParse(cfg["Resources:StartingGold"], out var startingGold) ? Math.Max(0, startingGold) : 100;
|
||||
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
||||
? Math.Max(0, goldPerMinute)
|
||||
: 10;
|
||||
|
||||
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
||||
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
||||
new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task<PlayerResources> EnsureAsync(string username)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var update = Builders<PlayerResources>.Update
|
||||
.SetOnInsert(x => x.Username, username)
|
||||
.SetOnInsert(x => x.Gold, _startingGold)
|
||||
.SetOnInsert(x => x.CreatedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastGoldGeneratedAtUtc, now)
|
||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
var resources = await _resources.FindOneAndUpdateAsync(
|
||||
x => x.Username == username,
|
||||
update,
|
||||
new FindOneAndUpdateOptions<PlayerResources>
|
||||
{
|
||||
IsUpsert = true,
|
||||
ReturnDocument = ReturnDocument.After
|
||||
});
|
||||
|
||||
return await ApplyGoldGenerationAsync(resources);
|
||||
}
|
||||
|
||||
public async Task<PlayerResources> GetAsync(string username)
|
||||
{
|
||||
var resources = await EnsureAsync(username);
|
||||
return await ApplyGoldGenerationAsync(resources);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
return await _resources.FindOneAndUpdateAsync(
|
||||
x => x.Username == resources.Username,
|
||||
update,
|
||||
new FindOneAndUpdateOptions<PlayerResources>
|
||||
{
|
||||
ReturnDocument = ReturnDocument.After
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,9 @@
|
||||
"HomeSearchRadius": 80,
|
||||
"VisibilityDepth": 3
|
||||
},
|
||||
"Resources": {
|
||||
"StartingGold": 100,
|
||||
"HeadquartersGoldPerMinute": 10
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ data:
|
||||
World__HomeMinDistance: "8"
|
||||
World__HomeSearchRadius: "80"
|
||||
World__VisibilityDepth: "3"
|
||||
Resources__StartingGold: "100"
|
||||
Resources__HeadquartersGoldPerMinute: "10"
|
||||
|
||||
Reference in New Issue
Block a user