adding log camps
This commit is contained in:
@@ -14,6 +14,7 @@ var resources_request: HTTPRequest
|
||||
var units_request: HTTPRequest
|
||||
var train_unit_request: HTTPRequest
|
||||
var claim_request: HTTPRequest
|
||||
var build_request: HTTPRequest
|
||||
var resources_timer: Timer
|
||||
var session: Dictionary = {}
|
||||
var escape_overlay: Control
|
||||
@@ -31,9 +32,11 @@ var training_started_unix := 0.0
|
||||
var training_completes_unix := 0.0
|
||||
var training_completion_refresh_requested := false
|
||||
var claim_button: Button
|
||||
var build_log_camp_button: Button
|
||||
var selected_q := 0
|
||||
var selected_r := 0
|
||||
var selected_can_claim := false
|
||||
var selected_can_build_log_camp := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -104,9 +107,9 @@ func _build_ui() -> void:
|
||||
legend.add_theme_constant_override("separation", 12)
|
||||
top_content.add_child(legend)
|
||||
|
||||
legend.add_child(_legend_item(Color(0.78, 0.62, 0.19), "Home"))
|
||||
legend.add_child(_legend_item(Color(0.26, 0.62, 0.32), "Yours"))
|
||||
legend.add_child(_legend_item(Color(0.58, 0.28, 0.24), "Rival"))
|
||||
legend.add_child(_legend_item(Color(1.0, 0.76, 0.16), "Home"))
|
||||
legend.add_child(_legend_item(Color(0.16, 0.86, 0.92), "Yours"))
|
||||
legend.add_child(_legend_item(Color(0.95, 0.22, 0.18), "Rival"))
|
||||
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"))
|
||||
|
||||
@@ -181,8 +184,8 @@ func _build_ui() -> void:
|
||||
bottom_content.add_child(coordinate_label)
|
||||
|
||||
unit_label = Label.new()
|
||||
unit_label.text = "Scouts: --"
|
||||
unit_label.custom_minimum_size = Vector2(88, 0)
|
||||
unit_label.text = "Scouts: -- | Queue: --"
|
||||
unit_label.custom_minimum_size = Vector2(142, 0)
|
||||
unit_label.add_theme_font_size_override("font_size", 14)
|
||||
unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78))
|
||||
bottom_content.add_child(unit_label)
|
||||
@@ -217,6 +220,11 @@ func _build_ui() -> void:
|
||||
claim_button.pressed.connect(_claim_selected_tile)
|
||||
bottom_content.add_child(claim_button)
|
||||
|
||||
build_log_camp_button = _small_button("Build Log Camp")
|
||||
build_log_camp_button.disabled = true
|
||||
build_log_camp_button.pressed.connect(_build_log_camp)
|
||||
bottom_content.add_child(build_log_camp_button)
|
||||
|
||||
http_request = HTTPRequest.new()
|
||||
http_request.request_completed.connect(_on_home_request_completed)
|
||||
add_child(http_request)
|
||||
@@ -241,6 +249,10 @@ func _build_ui() -> void:
|
||||
claim_request.request_completed.connect(_on_claim_request_completed)
|
||||
add_child(claim_request)
|
||||
|
||||
build_request = HTTPRequest.new()
|
||||
build_request.request_completed.connect(_on_build_request_completed)
|
||||
add_child(build_request)
|
||||
|
||||
resources_timer = Timer.new()
|
||||
resources_timer.wait_time = RESOURCE_REFRESH_SECONDS
|
||||
resources_timer.autostart = false
|
||||
@@ -259,8 +271,11 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
||||
selected_q = q
|
||||
selected_r = r
|
||||
selected_can_claim = terrain.contains("Unclaimed")
|
||||
selected_can_build_log_camp = terrain.contains("Forest") and terrain.contains("Your territory") and not terrain.contains("Log Camp")
|
||||
if claim_button:
|
||||
claim_button.disabled = not selected_can_claim
|
||||
if build_log_camp_button:
|
||||
build_log_camp_button.disabled = not selected_can_build_log_camp
|
||||
if coordinate_label:
|
||||
coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain]
|
||||
|
||||
@@ -412,7 +427,9 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
|
||||
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]
|
||||
var wood: int = int(resources.get("wood", 0))
|
||||
var wood_per_minute: int = int(resources.get("woodPerMinute", 0))
|
||||
gold_label.text = "Gold: %d (+%d/min) | Wood: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute]
|
||||
|
||||
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
|
||||
gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text)
|
||||
@@ -452,12 +469,13 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
||||
return
|
||||
|
||||
var units: Dictionary = parsed
|
||||
unit_label.text = "Scouts: %d" % int(units.get("completedScouts", 0))
|
||||
|
||||
var training: Array = []
|
||||
var raw_training = units.get("training", [])
|
||||
if typeof(raw_training) == TYPE_ARRAY:
|
||||
training = raw_training
|
||||
|
||||
unit_label.text = "Scouts: %d | Queue: %d" % [int(units.get("completedScouts", 0)), training.size()]
|
||||
|
||||
if training.is_empty():
|
||||
training_started_unix = 0.0
|
||||
training_completes_unix = 0.0
|
||||
@@ -572,6 +590,55 @@ func _on_claim_request_completed(result: int, response_code: int, _headers: Pack
|
||||
_refresh_player_state()
|
||||
|
||||
|
||||
func _build_log_camp() -> void:
|
||||
if not selected_can_build_log_camp:
|
||||
coordinate_label.text = "Select controlled Forest territory without a building."
|
||||
return
|
||||
|
||||
var access_token: String = str(session.get("accessToken", ""))
|
||||
if access_token.is_empty():
|
||||
coordinate_label.text = "No auth token available. Log Camp was not built."
|
||||
return
|
||||
|
||||
if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
|
||||
build_log_camp_button.disabled = true
|
||||
coordinate_label.text = "Building Log Camp at q:%d r:%d..." % [selected_q, selected_r]
|
||||
var error: int = build_request.request(
|
||||
LOCATIONS_BASE_URL + "/api/locations/build",
|
||||
PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer %s" % access_token
|
||||
]),
|
||||
HTTPClient.METHOD_POST,
|
||||
JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": "LogCamp" })
|
||||
)
|
||||
if error != OK:
|
||||
build_log_camp_button.disabled = false
|
||||
coordinate_label.text = "Could not contact the locations service."
|
||||
|
||||
|
||||
func _on_build_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var text: String = body.get_string_from_utf8()
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
coordinate_label.text = "Log Camp build request failed."
|
||||
build_log_camp_button.disabled = not selected_can_build_log_camp
|
||||
return
|
||||
|
||||
if response_code < 200 or response_code >= 300:
|
||||
coordinate_label.text = "Log Camp build failed: %s" % text
|
||||
build_log_camp_button.disabled = not selected_can_build_log_camp
|
||||
_refresh_player_state()
|
||||
return
|
||||
|
||||
selected_can_build_log_camp = false
|
||||
build_log_camp_button.disabled = true
|
||||
coordinate_label.text = "Log Camp built. Wood production increased."
|
||||
_load_visible_locations()
|
||||
_refresh_player_state()
|
||||
|
||||
|
||||
func _update_tick_bar() -> void:
|
||||
if not tick_bar or not tick_label:
|
||||
return
|
||||
|
||||
@@ -6,6 +6,9 @@ signal tile_selected(q: int, r: int, terrain: String)
|
||||
const HEX_SIZE := 42.0
|
||||
const ISO_Y_SCALE := 0.56
|
||||
const SQRT_3 := 1.7320508075688772
|
||||
const INFLUENCE_RADIUS_X := 54.0
|
||||
const INFLUENCE_RADIUS_Y := 30.0
|
||||
const INFLUENCE_LINK_WIDTH := 54.0
|
||||
|
||||
var camera_offset: Vector2 = Vector2.ZERO
|
||||
var zoom: float = 1.0
|
||||
@@ -101,10 +104,11 @@ func _draw() -> void:
|
||||
return _tile_center(int(a["q"]), int(a["r"])).y < _tile_center(int(b["q"]), int(b["r"])).y
|
||||
)
|
||||
|
||||
_draw_control_regions(sorted_tiles)
|
||||
|
||||
for tile in sorted_tiles:
|
||||
_draw_tile(tile)
|
||||
|
||||
|
||||
func _draw_grid_lines() -> void:
|
||||
var line_color: Color = Color(0.12, 0.17, 0.17, 0.35)
|
||||
for x in range(-8, 9):
|
||||
@@ -155,10 +159,55 @@ func _draw_tile(tile: Dictionary) -> void:
|
||||
draw_polyline(_closed_points(points), Color(0.95, 0.76, 0.28), 4.0)
|
||||
elif coord == home_tile:
|
||||
draw_polyline(_closed_points(points), Color(1.0, 0.88, 0.38), 3.0)
|
||||
elif is_owned:
|
||||
draw_polyline(_closed_points(points), Color(0.48, 0.95, 0.55), 3.0)
|
||||
elif is_rival:
|
||||
draw_polyline(_closed_points(points), Color(0.94, 0.36, 0.29), 3.0)
|
||||
|
||||
var visible_location: Dictionary = visible_tiles.get(key, {})
|
||||
var building_type: String = str(visible_location.get("buildingType", ""))
|
||||
if building_type == "LogCamp":
|
||||
_draw_log_camp_marker(center, height)
|
||||
|
||||
|
||||
func _draw_control_regions(sorted_tiles: Array) -> void:
|
||||
for control in ["owned", "rival"]:
|
||||
var control_tiles: Array = sorted_tiles.filter(func(tile: Dictionary) -> bool:
|
||||
return _tile_control(_tile_key(int(tile["q"]), int(tile["r"]))) == control
|
||||
)
|
||||
if control_tiles.is_empty():
|
||||
continue
|
||||
|
||||
_draw_control_layer(control_tiles, control, 1.34, 0.16, INFLUENCE_LINK_WIDTH + 18.0)
|
||||
_draw_control_layer(control_tiles, control, 1.08, 0.28, INFLUENCE_LINK_WIDTH)
|
||||
_draw_control_layer(control_tiles, control, 0.82, 0.20, INFLUENCE_LINK_WIDTH - 16.0)
|
||||
|
||||
|
||||
func _draw_control_layer(control_tiles: Array, control: String, layer_scale: float, alpha: float, link_width: float) -> void:
|
||||
var control_color: Color = _control_color(control)
|
||||
var layer_color := Color(control_color.r, control_color.g, control_color.b, alpha)
|
||||
var tile_lookup := {}
|
||||
|
||||
for tile in control_tiles:
|
||||
var q: int = int(tile["q"])
|
||||
var r: int = int(tile["r"])
|
||||
tile_lookup[_tile_key(q, r)] = true
|
||||
|
||||
for tile in control_tiles:
|
||||
var q: int = int(tile["q"])
|
||||
var r: int = int(tile["r"])
|
||||
var center: Vector2 = _tile_center(q, r)
|
||||
|
||||
for neighbor in _neighbor_coordinates(q, r):
|
||||
var neighbor_key: String = _tile_key(neighbor.x, neighbor.y)
|
||||
if not tile_lookup.has(neighbor_key):
|
||||
continue
|
||||
if _tile_key(q, r) > neighbor_key:
|
||||
continue
|
||||
|
||||
var neighbor_center: Vector2 = _tile_center(neighbor.x, neighbor.y)
|
||||
draw_line(center, neighbor_center, layer_color, link_width * zoom * layer_scale)
|
||||
|
||||
draw_colored_polygon(
|
||||
_ellipse_points(center, INFLUENCE_RADIUS_X * zoom * layer_scale, INFLUENCE_RADIUS_Y * zoom * layer_scale),
|
||||
layer_color
|
||||
)
|
||||
|
||||
|
||||
func _update_hover(mouse_position: Vector2) -> void:
|
||||
@@ -202,11 +251,15 @@ func _screen_point(world_point: Vector2) -> Vector2:
|
||||
|
||||
|
||||
func _hex_points(center: Vector2, height: float) -> PackedVector2Array:
|
||||
return _hex_points_scaled(center, height, 1.0)
|
||||
|
||||
|
||||
func _hex_points_scaled(center: Vector2, height: float, point_scale: float) -> PackedVector2Array:
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
var top_lift: Vector2 = Vector2(0, -height * zoom)
|
||||
for i in range(6):
|
||||
var angle: float = deg_to_rad(60.0 * i + 30.0)
|
||||
var local: Vector2 = Vector2(cos(angle) * HEX_SIZE, sin(angle) * HEX_SIZE * ISO_Y_SCALE) * zoom
|
||||
var local: Vector2 = Vector2(cos(angle) * HEX_SIZE * point_scale, sin(angle) * HEX_SIZE * ISO_Y_SCALE * point_scale) * zoom
|
||||
points.append(center + local + top_lift)
|
||||
return points
|
||||
|
||||
@@ -218,6 +271,33 @@ func _closed_points(points: PackedVector2Array) -> PackedVector2Array:
|
||||
return closed
|
||||
|
||||
|
||||
func _ellipse_points(center: Vector2, radius_x: float, radius_y: float) -> PackedVector2Array:
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(32):
|
||||
var angle: float = TAU * float(i) / 32.0
|
||||
points.append(center + Vector2(cos(angle) * radius_x, sin(angle) * radius_y))
|
||||
return points
|
||||
|
||||
|
||||
func _draw_log_camp_marker(center: Vector2, height: float) -> void:
|
||||
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 2.0)
|
||||
var width: float = 18.0 * zoom
|
||||
var marker_height: float = 10.0 * zoom
|
||||
var hut_color := Color(0.46, 0.29, 0.14)
|
||||
var roof_color := Color(0.28, 0.17, 0.09)
|
||||
var log_color := Color(0.72, 0.52, 0.28)
|
||||
|
||||
var roof := PackedVector2Array([
|
||||
marker_center + Vector2(-width * 0.55, -marker_height * 0.12),
|
||||
marker_center + Vector2(0, -marker_height * 0.75),
|
||||
marker_center + Vector2(width * 0.55, -marker_height * 0.12)
|
||||
])
|
||||
draw_colored_polygon(roof, roof_color)
|
||||
draw_rect(Rect2(marker_center + Vector2(-width * 0.38, -marker_height * 0.08), Vector2(width * 0.76, marker_height * 0.62)), hut_color, true)
|
||||
draw_line(marker_center + Vector2(-width * 0.48, marker_height * 0.62), marker_center + Vector2(width * 0.48, marker_height * 0.62), log_color, 3.0 * zoom)
|
||||
draw_line(marker_center + Vector2(-width * 0.38, marker_height * 0.86), marker_center + Vector2(width * 0.38, marker_height * 0.86), log_color.darkened(0.12), 3.0 * zoom)
|
||||
|
||||
|
||||
func _tile_data(q: int, r: int) -> Dictionary:
|
||||
for tile in tiles:
|
||||
if int(tile["q"]) == q and int(tile["r"]) == r:
|
||||
@@ -269,27 +349,61 @@ func _ownership_fill_color(base_color: Color, coord: Vector2i, tile_visible: boo
|
||||
if not tile_visible:
|
||||
return Color(0.045, 0.065, 0.065)
|
||||
if coord == home_tile:
|
||||
return Color(0.78, 0.62, 0.19)
|
||||
return base_color.lerp(Color(0.95, 0.72, 0.18), 0.38)
|
||||
if is_owned:
|
||||
return Color(0.26, 0.62, 0.32)
|
||||
return base_color.lerp(Color(0.20, 0.68, 0.72), 0.32)
|
||||
if is_rival:
|
||||
return Color(0.58, 0.28, 0.24)
|
||||
return base_color.lerp(Color(0.72, 0.22, 0.18), 0.35)
|
||||
return base_color
|
||||
|
||||
|
||||
func _tile_control(key: String) -> String:
|
||||
if owned_tiles.has(key):
|
||||
return "owned"
|
||||
if rival_tiles.has(key):
|
||||
return "rival"
|
||||
return ""
|
||||
|
||||
|
||||
func _control_color(control: String) -> Color:
|
||||
match control:
|
||||
"owned":
|
||||
return Color(0.16, 0.86, 0.92)
|
||||
"rival":
|
||||
return Color(0.95, 0.22, 0.18)
|
||||
_:
|
||||
return Color(0.4, 0.5, 0.5)
|
||||
|
||||
|
||||
func _neighbor_coordinates(q: int, r: int) -> Array[Vector2i]:
|
||||
return [
|
||||
Vector2i(q + 1, r),
|
||||
Vector2i(q + 1, r - 1),
|
||||
Vector2i(q, r - 1),
|
||||
Vector2i(q - 1, r),
|
||||
Vector2i(q - 1, r + 1),
|
||||
Vector2i(q, r + 1)
|
||||
]
|
||||
|
||||
|
||||
func _tile_display_label(key: String, data: Dictionary) -> String:
|
||||
var terrain: String = str(data.get("terrain", "Open"))
|
||||
var building_type: String = str(data.get("buildingType", ""))
|
||||
var building_label: String = ""
|
||||
if building_type == "LogCamp":
|
||||
building_label = " | Log Camp"
|
||||
|
||||
if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))):
|
||||
return "%s | Your home" % terrain
|
||||
return "%s | Your home%s" % [terrain, building_label]
|
||||
if owned_tiles.has(key):
|
||||
return "%s | Your territory" % terrain
|
||||
return "%s | Your territory%s" % [terrain, building_label]
|
||||
if rival_tiles.has(key):
|
||||
var rival_data: Dictionary = rival_tiles[key]
|
||||
var owner_username: String = _owner_username(rival_data)
|
||||
if owner_username.is_empty():
|
||||
return "%s | Unclaimed" % terrain
|
||||
return "%s | Controlled by %s" % [terrain, owner_username]
|
||||
return "%s | Unclaimed" % terrain
|
||||
return "%s | Unclaimed%s" % [terrain, building_label]
|
||||
return "%s | Controlled by %s%s" % [terrain, owner_username, building_label]
|
||||
return "%s | Unclaimed%s" % [terrain, building_label]
|
||||
|
||||
|
||||
func _owner_username(location: Dictionary) -> String:
|
||||
|
||||
@@ -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