adding resources
This commit is contained in:
@@ -9,7 +9,11 @@ var title_font: Font
|
|||||||
var grid_view
|
var grid_view
|
||||||
var http_request: HTTPRequest
|
var http_request: HTTPRequest
|
||||||
var visible_request: HTTPRequest
|
var visible_request: HTTPRequest
|
||||||
|
var resources_request: HTTPRequest
|
||||||
var session: Dictionary = {}
|
var session: Dictionary = {}
|
||||||
|
var escape_overlay: Control
|
||||||
|
var escape_panel: PanelContainer
|
||||||
|
var gold_label: Label
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -18,6 +22,12 @@ func _ready() -> void:
|
|||||||
_assign_home_location()
|
_assign_home_location()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
_toggle_escape_menu()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
title_font = load("res://assets/ui/kenney_ui_pack/Font/Kenney Future.ttf")
|
title_font = load("res://assets/ui/kenney_ui_pack/Font/Kenney Future.ttf")
|
||||||
@@ -75,6 +85,12 @@ func _build_ui() -> void:
|
|||||||
legend.add_child(_legend_item(Color(0.38, 0.56, 0.36), "Open"))
|
legend.add_child(_legend_item(Color(0.38, 0.56, 0.36), "Open"))
|
||||||
legend.add_child(_legend_item(Color(0.045, 0.065, 0.065), "Unknown"))
|
legend.add_child(_legend_item(Color(0.045, 0.065, 0.065), "Unknown"))
|
||||||
|
|
||||||
|
gold_label = Label.new()
|
||||||
|
gold_label.text = "Gold: --"
|
||||||
|
gold_label.add_theme_font_size_override("font_size", 15)
|
||||||
|
gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32))
|
||||||
|
top_content.add_child(gold_label)
|
||||||
|
|
||||||
var user_label := Label.new()
|
var user_label := Label.new()
|
||||||
user_label.text = _session_name()
|
user_label.text = _session_name()
|
||||||
user_label.add_theme_font_size_override("font_size", 15)
|
user_label.add_theme_font_size_override("font_size", 15)
|
||||||
@@ -121,6 +137,12 @@ func _build_ui() -> void:
|
|||||||
visible_request.request_completed.connect(_on_visible_request_completed)
|
visible_request.request_completed.connect(_on_visible_request_completed)
|
||||||
add_child(visible_request)
|
add_child(visible_request)
|
||||||
|
|
||||||
|
resources_request = HTTPRequest.new()
|
||||||
|
resources_request.request_completed.connect(_on_resources_request_completed)
|
||||||
|
add_child(resources_request)
|
||||||
|
|
||||||
|
_build_escape_menu()
|
||||||
|
|
||||||
|
|
||||||
func _on_tile_hovered(q: int, r: int, terrain: String) -> void:
|
func _on_tile_hovered(q: int, r: int, terrain: String) -> void:
|
||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
@@ -237,6 +259,148 @@ func _on_visible_request_completed(result: int, response_code: int, _headers: Pa
|
|||||||
var visible_locations: Array = parsed
|
var visible_locations: Array = parsed
|
||||||
grid_view.set_visible_locations(visible_locations, username)
|
grid_view.set_visible_locations(visible_locations, username)
|
||||||
coordinate_label.text = "Visible territory loaded. Sight extends 3 hexes beyond controlled land."
|
coordinate_label.text = "Visible territory loaded. Sight extends 3 hexes beyond controlled land."
|
||||||
|
_load_resources()
|
||||||
|
|
||||||
|
|
||||||
|
func _load_resources() -> void:
|
||||||
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
|
if access_token.is_empty():
|
||||||
|
gold_label.text = "Gold: --"
|
||||||
|
return
|
||||||
|
|
||||||
|
var error: int = resources_request.request(
|
||||||
|
LOCATIONS_BASE_URL + "/api/resources/me",
|
||||||
|
PackedStringArray([
|
||||||
|
"Authorization: Bearer %s" % access_token
|
||||||
|
]),
|
||||||
|
HTTPClient.METHOD_GET
|
||||||
|
)
|
||||||
|
if error != OK:
|
||||||
|
gold_label.text = "Gold: ?"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resources_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
gold_label.text = "Gold: ?"
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed = JSON.parse_string(body.get_string_from_utf8())
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
gold_label.text = "Gold: ?"
|
||||||
|
return
|
||||||
|
|
||||||
|
var resources: Dictionary = parsed
|
||||||
|
var gold: int = int(resources.get("gold", 0))
|
||||||
|
var gold_per_minute: int = int(resources.get("goldPerMinute", 0))
|
||||||
|
gold_label.text = "Gold: %d (+%d/min)" % [gold, gold_per_minute]
|
||||||
|
|
||||||
|
|
||||||
|
func _build_escape_menu() -> void:
|
||||||
|
escape_overlay = Control.new()
|
||||||
|
escape_overlay.visible = false
|
||||||
|
escape_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
escape_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(escape_overlay)
|
||||||
|
|
||||||
|
var scrim := ColorRect.new()
|
||||||
|
scrim.color = Color(0.01, 0.015, 0.015, 0.64)
|
||||||
|
scrim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
escape_overlay.add_child(scrim)
|
||||||
|
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
escape_overlay.add_child(center)
|
||||||
|
|
||||||
|
escape_panel = PanelContainer.new()
|
||||||
|
escape_panel.custom_minimum_size = Vector2(360, 310)
|
||||||
|
escape_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.09, 0.13, 0.13, 0.98), Color(0.34, 0.49, 0.46), 2, 8))
|
||||||
|
center.add_child(escape_panel)
|
||||||
|
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 30)
|
||||||
|
margin.add_theme_constant_override("margin_top", 28)
|
||||||
|
margin.add_theme_constant_override("margin_right", 30)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 26)
|
||||||
|
escape_panel.add_child(margin)
|
||||||
|
|
||||||
|
var menu := VBoxContainer.new()
|
||||||
|
menu.add_theme_constant_override("separation", 14)
|
||||||
|
margin.add_child(menu)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Paused"
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
title.add_theme_color_override("font_color", Color(0.94, 0.96, 0.9))
|
||||||
|
if title_font:
|
||||||
|
title.add_theme_font_override("font", title_font)
|
||||||
|
menu.add_child(title)
|
||||||
|
|
||||||
|
var account := Label.new()
|
||||||
|
account.text = _session_name()
|
||||||
|
account.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
account.add_theme_font_size_override("font_size", 14)
|
||||||
|
account.add_theme_color_override("font_color", Color(0.64, 0.78, 0.74))
|
||||||
|
menu.add_child(account)
|
||||||
|
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.custom_minimum_size = Vector2(0, 10)
|
||||||
|
menu.add_child(spacer)
|
||||||
|
|
||||||
|
var resume_button := _menu_button("Resume")
|
||||||
|
resume_button.pressed.connect(_toggle_escape_menu)
|
||||||
|
menu.add_child(resume_button)
|
||||||
|
|
||||||
|
var logout_button := _menu_button("Log Out")
|
||||||
|
logout_button.pressed.connect(_logout)
|
||||||
|
menu.add_child(logout_button)
|
||||||
|
|
||||||
|
var quit_button := _menu_button("Quit")
|
||||||
|
quit_button.pressed.connect(func() -> void: get_tree().quit())
|
||||||
|
menu.add_child(quit_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_escape_menu() -> void:
|
||||||
|
if not escape_overlay:
|
||||||
|
return
|
||||||
|
|
||||||
|
escape_overlay.visible = not escape_overlay.visible
|
||||||
|
if escape_overlay.visible and escape_panel:
|
||||||
|
var first_button := _find_first_button(escape_panel)
|
||||||
|
if first_button:
|
||||||
|
first_button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _logout() -> void:
|
||||||
|
if FileAccess.file_exists(SESSION_PATH):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(SESSION_PATH))
|
||||||
|
get_tree().remove_meta("auth_session")
|
||||||
|
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func _menu_button(text_value: String) -> Button:
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = text_value
|
||||||
|
button.custom_minimum_size = Vector2(0, 48)
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.add_theme_font_size_override("font_size", 17)
|
||||||
|
button.add_theme_stylebox_override("normal", _button_style(Color(0.75, 0.78, 0.68)))
|
||||||
|
button.add_theme_stylebox_override("hover", _button_style(Color(0.88, 0.82, 0.47)))
|
||||||
|
button.add_theme_stylebox_override("pressed", _button_style(Color(0.58, 0.62, 0.52)))
|
||||||
|
button.add_theme_color_override("font_color", Color(0.08, 0.12, 0.11))
|
||||||
|
button.add_theme_color_override("font_hover_color", Color(0.08, 0.12, 0.11))
|
||||||
|
button.add_theme_color_override("font_pressed_color", Color(0.08, 0.12, 0.11))
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
func _find_first_button(node: Node) -> Button:
|
||||||
|
if node is Button:
|
||||||
|
return node
|
||||||
|
for child in node.get_children():
|
||||||
|
var button := _find_first_button(child)
|
||||||
|
if button:
|
||||||
|
return button
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func _panel_style(color: Color, border_color: Color, border_width: int, radius: int) -> StyleBoxFlat:
|
func _panel_style(color: Color, border_color: Color, border_width: int, radius: int) -> StyleBoxFlat:
|
||||||
@@ -248,6 +412,15 @@ func _panel_style(color: Color, border_color: Color, border_width: int, radius:
|
|||||||
return style
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _button_style(color: Color) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = color
|
||||||
|
style.border_color = color.darkened(0.28)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(6)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
func _legend_item(color: Color, label_text: String) -> HBoxContainer:
|
func _legend_item(color: Color, label_text: String) -> HBoxContainer:
|
||||||
var item := HBoxContainer.new()
|
var item := HBoxContainer.new()
|
||||||
item.add_theme_constant_override("separation", 5)
|
item.add_theme_constant_override("separation", 5)
|
||||||
|
|||||||
@@ -126,15 +126,15 @@ func _draw_tile(tile: Dictionary) -> void:
|
|||||||
var is_hovered: bool = coord == hovered_tile
|
var is_hovered: bool = coord == hovered_tile
|
||||||
var is_selected: bool = coord == selected_tile
|
var is_selected: bool = coord == selected_tile
|
||||||
var key: String = _tile_key(q, r)
|
var key: String = _tile_key(q, r)
|
||||||
var is_visible: bool = visible_tiles.has(key)
|
var tile_visible: bool = visible_tiles.has(key)
|
||||||
var is_owned: bool = owned_tiles.has(key)
|
var is_owned: bool = owned_tiles.has(key)
|
||||||
var is_rival: bool = rival_tiles.has(key)
|
var is_rival: bool = rival_tiles.has(key)
|
||||||
|
|
||||||
var side_points: PackedVector2Array = PackedVector2Array([points[2], points[3], points[4], points[4] + Vector2(0, 14), points[3] + Vector2(0, 14), points[2] + Vector2(0, 14)])
|
var side_points: PackedVector2Array = PackedVector2Array([points[2], points[3], points[4], points[4] + Vector2(0, 14), points[3] + Vector2(0, 14), points[2] + Vector2(0, 14)])
|
||||||
var draw_color: Color = _ownership_fill_color(color, coord, is_visible, is_owned, is_rival)
|
var draw_color: Color = _ownership_fill_color(color, coord, tile_visible, is_owned, is_rival)
|
||||||
draw_colored_polygon(side_points, draw_color.darkened(0.28))
|
draw_colored_polygon(side_points, draw_color.darkened(0.28))
|
||||||
|
|
||||||
if not is_visible:
|
if not tile_visible:
|
||||||
draw_colored_polygon(points, draw_color)
|
draw_colored_polygon(points, draw_color)
|
||||||
elif is_selected:
|
elif is_selected:
|
||||||
draw_colored_polygon(points, draw_color.lightened(0.2))
|
draw_colored_polygon(points, draw_color.lightened(0.2))
|
||||||
@@ -147,7 +147,7 @@ func _draw_tile(tile: Dictionary) -> void:
|
|||||||
|
|
||||||
draw_polyline(_closed_points(points), Color(0.04, 0.065, 0.06, 0.72), 2.0)
|
draw_polyline(_closed_points(points), Color(0.04, 0.065, 0.06, 0.72), 2.0)
|
||||||
|
|
||||||
if not is_visible:
|
if not tile_visible:
|
||||||
draw_polyline(_closed_points(points), Color(0.11, 0.15, 0.15, 0.75), 1.0)
|
draw_polyline(_closed_points(points), Color(0.11, 0.15, 0.15, 0.75), 1.0)
|
||||||
elif is_selected:
|
elif is_selected:
|
||||||
draw_polyline(_closed_points(points), Color(0.95, 0.76, 0.28), 4.0)
|
draw_polyline(_closed_points(points), Color(0.95, 0.76, 0.28), 4.0)
|
||||||
@@ -244,14 +244,14 @@ func set_visible_locations(locations: Array, username: String) -> void:
|
|||||||
var q: int = int(location.get("q", 0))
|
var q: int = int(location.get("q", 0))
|
||||||
var r: int = int(location.get("r", 0))
|
var r: int = int(location.get("r", 0))
|
||||||
var terrain: String = str(location.get("terrain", "Field"))
|
var terrain: String = str(location.get("terrain", "Field"))
|
||||||
var owner: String = _owner_username(location)
|
var owner_username: String = _owner_username(location)
|
||||||
var key: String = _tile_key(q, r)
|
var key: String = _tile_key(q, r)
|
||||||
|
|
||||||
visible_tiles[key] = location
|
visible_tiles[key] = location
|
||||||
_add_or_update_tile(q, r, terrain)
|
_add_or_update_tile(q, r, terrain)
|
||||||
|
|
||||||
if not owner.is_empty():
|
if not owner_username.is_empty():
|
||||||
if owner == username:
|
if owner_username == username:
|
||||||
owned_tiles[key] = location
|
owned_tiles[key] = location
|
||||||
else:
|
else:
|
||||||
rival_tiles[key] = location
|
rival_tiles[key] = location
|
||||||
@@ -263,8 +263,8 @@ func _tile_key(q: int, r: int) -> String:
|
|||||||
return "%d,%d" % [q, r]
|
return "%d,%d" % [q, r]
|
||||||
|
|
||||||
|
|
||||||
func _ownership_fill_color(base_color: Color, coord: Vector2i, is_visible: bool, is_owned: bool, is_rival: bool) -> Color:
|
func _ownership_fill_color(base_color: Color, coord: Vector2i, tile_visible: bool, is_owned: bool, is_rival: bool) -> Color:
|
||||||
if not is_visible:
|
if not tile_visible:
|
||||||
return Color(0.045, 0.065, 0.065)
|
return Color(0.045, 0.065, 0.065)
|
||||||
if coord == home_tile:
|
if coord == home_tile:
|
||||||
return Color(0.78, 0.62, 0.19)
|
return Color(0.78, 0.62, 0.19)
|
||||||
@@ -283,10 +283,10 @@ func _tile_display_label(key: String, data: Dictionary) -> String:
|
|||||||
return "%s | Your territory" % terrain
|
return "%s | Your territory" % terrain
|
||||||
if rival_tiles.has(key):
|
if rival_tiles.has(key):
|
||||||
var rival_data: Dictionary = rival_tiles[key]
|
var rival_data: Dictionary = rival_tiles[key]
|
||||||
var owner: String = _owner_username(rival_data)
|
var owner_username: String = _owner_username(rival_data)
|
||||||
if owner.is_empty():
|
if owner_username.is_empty():
|
||||||
return "%s | Unclaimed" % terrain
|
return "%s | Unclaimed" % terrain
|
||||||
return "%s | Controlled by %s" % [terrain, owner]
|
return "%s | Controlled by %s" % [terrain, owner_username]
|
||||||
return "%s | Unclaimed" % terrain
|
return "%s | Unclaimed" % terrain
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.AddControllers();
|
||||||
builder.Services.AddSingleton<LocationService>();
|
builder.Services.AddSingleton<LocationService>();
|
||||||
|
builder.Services.AddSingleton<ResourceService>();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
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` Get locations visible to the authenticated user.
|
||||||
- `GET /api/locations/visible?depth=3` Override visibility depth for a request.
|
- `GET /api/locations/visible?depth=3` Override visibility depth for a request.
|
||||||
- `PUT /api/locations` Upsert location data (SUPER only).
|
- `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.
|
`claim` and `put` require the same JWT issued by AuthApi.
|
||||||
|
|
||||||
When a player gains visibility over previously unseen coordinates, the service
|
When a player gains visibility over previously unseen coordinates, the service
|
||||||
generates and persists those locations before returning them. Generated visible
|
generates and persists those locations before returning them. Generated visible
|
||||||
locations are unowned `Open` records until claimed by a player.
|
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
|
public class LocationService
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<Location> _locations;
|
private readonly IMongoCollection<Location> _locations;
|
||||||
|
private readonly ResourceService _resources;
|
||||||
private readonly int _homeMinDistance;
|
private readonly int _homeMinDistance;
|
||||||
private readonly int _homeSearchRadius;
|
private readonly int _homeSearchRadius;
|
||||||
private readonly int _visibilityDepth;
|
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 connectionString = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||||
var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler";
|
var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler";
|
||||||
var client = new MongoClient(connectionString);
|
var client = new MongoClient(connectionString);
|
||||||
var database = client.GetDatabase(databaseName);
|
var database = client.GetDatabase(databaseName);
|
||||||
_locations = database.GetCollection<Location>("Locations");
|
_locations = database.GetCollection<Location>("Locations");
|
||||||
|
_resources = resources;
|
||||||
_homeMinDistance = int.TryParse(cfg["World:HomeMinDistance"], out var minDistance) ? Math.Max(1, minDistance) : 8;
|
_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;
|
_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;
|
_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);
|
var existingHome = await GetHomeAsync(username);
|
||||||
if (existingHome is not null)
|
if (existingHome is not null)
|
||||||
{
|
{
|
||||||
|
await _resources.EnsureAsync(username);
|
||||||
return existingHome;
|
return existingHome;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +141,7 @@ public class LocationService
|
|||||||
var home = await TryCreateHomeAsync(username, candidate.q, candidate.r);
|
var home = await TryCreateHomeAsync(username, candidate.q, candidate.r);
|
||||||
if (home is not null)
|
if (home is not null)
|
||||||
{
|
{
|
||||||
|
await _resources.EnsureAsync(username);
|
||||||
return home;
|
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,
|
"HomeSearchRadius": 80,
|
||||||
"VisibilityDepth": 3
|
"VisibilityDepth": 3
|
||||||
},
|
},
|
||||||
|
"Resources": {
|
||||||
|
"StartingGold": 100,
|
||||||
|
"HeadquartersGoldPerMinute": 10
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ data:
|
|||||||
World__HomeMinDistance: "8"
|
World__HomeMinDistance: "8"
|
||||||
World__HomeSearchRadius: "80"
|
World__HomeSearchRadius: "80"
|
||||||
World__VisibilityDepth: "3"
|
World__VisibilityDepth: "3"
|
||||||
|
Resources__StartingGold: "100"
|
||||||
|
Resources__HeadquartersGoldPerMinute: "10"
|
||||||
|
|||||||
Reference in New Issue
Block a user