updating unit movement
This commit is contained in:
@@ -498,9 +498,9 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
|
|||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Archers can only move to an adjacent tile."
|
coordinate_label.text = "Archers can only move to an adjacent tile."
|
||||||
return
|
return
|
||||||
if not grid_view.is_visible_non_rival_tile(q, r):
|
if not grid_view.is_visible_tile(q, r):
|
||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Archers can only move to visible non-rival tiles."
|
coordinate_label.text = "Archers can only move to visible tiles."
|
||||||
return
|
return
|
||||||
|
|
||||||
pending_move_q = q
|
pending_move_q = q
|
||||||
@@ -1037,8 +1037,8 @@ func _move_selected_archers(to_q: int, to_r: int) -> void:
|
|||||||
if not grid_view.is_neighbor(selected_archer_q, selected_archer_r, to_q, to_r):
|
if not grid_view.is_neighbor(selected_archer_q, selected_archer_r, to_q, to_r):
|
||||||
coordinate_label.text = "Archers can only move to an adjacent tile."
|
coordinate_label.text = "Archers can only move to an adjacent tile."
|
||||||
return
|
return
|
||||||
if not grid_view.is_visible_non_rival_tile(to_q, to_r):
|
if not grid_view.is_visible_tile(to_q, to_r):
|
||||||
coordinate_label.text = "Archers can only move to visible non-rival tiles."
|
coordinate_label.text = "Archers can only move to visible tiles."
|
||||||
return
|
return
|
||||||
|
|
||||||
var access_token: String = str(session.get("accessToken", ""))
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ func _gui_input(event: InputEvent) -> void:
|
|||||||
if event.pressed:
|
if event.pressed:
|
||||||
drag_distance = 0.0
|
drag_distance = 0.0
|
||||||
if not event.pressed and drag_distance < 6.0 and hovered_tile.x != 9999:
|
if not event.pressed and drag_distance < 6.0 and hovered_tile.x != 9999:
|
||||||
selected_tile = hovered_tile
|
var clicked_unit_tile := _archer_tile_at(event.position)
|
||||||
|
selected_tile = clicked_unit_tile if clicked_unit_tile.x != 9999 else hovered_tile
|
||||||
var data: Dictionary = _tile_data(selected_tile.x, selected_tile.y)
|
var data: Dictionary = _tile_data(selected_tile.x, selected_tile.y)
|
||||||
var key: String = _tile_key(selected_tile.x, selected_tile.y)
|
var key: String = _tile_key(selected_tile.x, selected_tile.y)
|
||||||
var label: String = _tile_display_label(key, data) if visible_tiles.has(key) else "Unexplored"
|
var label: String = _tile_display_label(key, data) if visible_tiles.has(key) else "Unexplored"
|
||||||
@@ -442,7 +443,7 @@ func _draw_building_marker(center: Vector2, height: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _draw_archer_unit_marker(center: Vector2, height: float, count: int) -> void:
|
func _draw_archer_unit_marker(center: Vector2, height: float, count: int) -> void:
|
||||||
var marker_center: Vector2 = center + Vector2(18.0 * zoom, -height * zoom - 14.0 * zoom)
|
var marker_center: Vector2 = _archer_marker_center(center, height)
|
||||||
var marker_scale: float = zoom
|
var marker_scale: float = zoom
|
||||||
var cloak_color := Color(0.20, 0.38, 0.26)
|
var cloak_color := Color(0.20, 0.38, 0.26)
|
||||||
var hood_color := Color(0.12, 0.23, 0.16)
|
var hood_color := Color(0.12, 0.23, 0.16)
|
||||||
@@ -487,6 +488,31 @@ func _draw_archer_unit_marker(center: Vector2, height: float, count: int) -> voi
|
|||||||
draw_string(font, badge_rect.position + Vector2(3.5, 9.0) * marker_scale, count_text, HORIZONTAL_ALIGNMENT_LEFT, -1, badge_font_size, Color(0.93, 0.86, 0.58))
|
draw_string(font, badge_rect.position + Vector2(3.5, 9.0) * marker_scale, count_text, HORIZONTAL_ALIGNMENT_LEFT, -1, badge_font_size, Color(0.93, 0.86, 0.58))
|
||||||
|
|
||||||
|
|
||||||
|
func _archer_marker_center(center: Vector2, height: float) -> Vector2:
|
||||||
|
return center + Vector2(18.0 * zoom, -height * zoom - 14.0 * zoom)
|
||||||
|
|
||||||
|
|
||||||
|
func _archer_tile_at(mouse_position: Vector2) -> Vector2i:
|
||||||
|
var closest := Vector2i(9999, 9999)
|
||||||
|
var closest_distance := 999999.0
|
||||||
|
for tile in tiles:
|
||||||
|
var q: int = int(tile["q"])
|
||||||
|
var r: int = int(tile["r"])
|
||||||
|
if _archer_count_at(q, r) <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var center: Vector2 = _tile_center(q, r)
|
||||||
|
var height: float = float(tile["height"])
|
||||||
|
var distance: float = _archer_marker_center(center, height).distance_to(mouse_position)
|
||||||
|
if distance < closest_distance:
|
||||||
|
closest_distance = distance
|
||||||
|
closest = Vector2i(q, r)
|
||||||
|
|
||||||
|
if closest_distance > 26.0 * zoom:
|
||||||
|
return Vector2i(9999, 9999)
|
||||||
|
return closest
|
||||||
|
|
||||||
|
|
||||||
func _tile_data(q: int, r: int) -> Dictionary:
|
func _tile_data(q: int, r: int) -> Dictionary:
|
||||||
for tile in tiles:
|
for tile in tiles:
|
||||||
if int(tile["q"]) == q and int(tile["r"]) == r:
|
if int(tile["q"]) == q and int(tile["r"]) == r:
|
||||||
@@ -583,6 +609,10 @@ func is_visible_non_rival_tile(q: int, r: int) -> bool:
|
|||||||
return visible_tiles.has(key) and not rival_tiles.has(key)
|
return visible_tiles.has(key) and not rival_tiles.has(key)
|
||||||
|
|
||||||
|
|
||||||
|
func is_visible_tile(q: int, r: int) -> bool:
|
||||||
|
return visible_tiles.has(_tile_key(q, r))
|
||||||
|
|
||||||
|
|
||||||
func _tile_key(q: int, r: int) -> String:
|
func _tile_key(q: int, r: int) -> String:
|
||||||
return "%d,%d" % [q, r]
|
return "%d,%d" % [q, r]
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class LocationsController : ControllerBase
|
|||||||
? await _locations.GetRangeAsync(minQ.Value, maxQ.Value, minR.Value, maxR.Value)
|
? await _locations.GetRangeAsync(minQ.Value, maxQ.Value, minR.Value, maxR.Value)
|
||||||
: await _locations.GetAllAsync();
|
: await _locations.GetAllAsync();
|
||||||
|
|
||||||
return Ok(locations.Select(LocationResponse.FromLocation).ToList());
|
return Ok(locations.Select(location => LocationResponse.FromLocation(location)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class LocationResponse
|
|||||||
public DateTime UpdatedAtUtc { get; set; }
|
public DateTime UpdatedAtUtc { get; set; }
|
||||||
public bool IsVisible { get; set; } = true;
|
public bool IsVisible { get; set; } = true;
|
||||||
|
|
||||||
public static LocationResponse FromLocation(Location location) => new()
|
public static LocationResponse FromLocation(Location location, bool revealDetails = true) => new()
|
||||||
{
|
{
|
||||||
Id = location.Id,
|
Id = location.Id,
|
||||||
Q = location.Q,
|
Q = location.Q,
|
||||||
@@ -51,16 +51,38 @@ public class LocationResponse
|
|||||||
OwnerUsername = location.OwnerUsername,
|
OwnerUsername = location.OwnerUsername,
|
||||||
Label = location.Label,
|
Label = location.Label,
|
||||||
Level = location.Level,
|
Level = location.Level,
|
||||||
BuildingType = location.BuildingType,
|
BuildingType = revealDetails ? location.BuildingType : null,
|
||||||
BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
BuildingStatus = revealDetails && (location.BuildingType == "LogCamp" || location.BuildingType == "Mine" || location.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||||
? "Built"
|
? "Built"
|
||||||
: location.BuildingStatus,
|
: revealDetails ? location.BuildingStatus : null,
|
||||||
BuildingStartedAtUtc = location.BuildingStartedAtUtc,
|
BuildingStartedAtUtc = revealDetails ? location.BuildingStartedAtUtc : null,
|
||||||
BuildingCompletesAtUtc = location.BuildingCompletesAtUtc,
|
BuildingCompletesAtUtc = revealDetails ? location.BuildingCompletesAtUtc : null,
|
||||||
BuildingBuiltAtUtc = location.BuildingBuiltAtUtc,
|
BuildingBuiltAtUtc = revealDetails ? location.BuildingBuiltAtUtc : null,
|
||||||
HomeAssignedAtUtc = location.HomeAssignedAtUtc,
|
HomeAssignedAtUtc = location.HomeAssignedAtUtc,
|
||||||
UpdatedAtUtc = location.UpdatedAtUtc,
|
UpdatedAtUtc = location.UpdatedAtUtc,
|
||||||
IsVisible = true
|
IsVisible = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static LocationResponse FromIntel(LocationIntel intel) => new()
|
||||||
|
{
|
||||||
|
Id = intel.Id,
|
||||||
|
Q = intel.Q,
|
||||||
|
R = intel.R,
|
||||||
|
Terrain = intel.Terrain,
|
||||||
|
Kind = intel.Kind,
|
||||||
|
OwnerUsername = intel.OwnerUsername,
|
||||||
|
Label = intel.Label,
|
||||||
|
Level = intel.Level,
|
||||||
|
BuildingType = intel.BuildingType,
|
||||||
|
BuildingStatus = (intel.BuildingType == "LogCamp" || intel.BuildingType == "Mine" || intel.BuildingType == "Barracks") && string.IsNullOrWhiteSpace(intel.BuildingStatus)
|
||||||
|
? "Built"
|
||||||
|
: intel.BuildingStatus,
|
||||||
|
BuildingStartedAtUtc = intel.BuildingStartedAtUtc,
|
||||||
|
BuildingCompletesAtUtc = intel.BuildingCompletesAtUtc,
|
||||||
|
BuildingBuiltAtUtc = intel.BuildingBuiltAtUtc,
|
||||||
|
HomeAssignedAtUtc = intel.HomeAssignedAtUtc,
|
||||||
|
UpdatedAtUtc = intel.ObservedAtUtc,
|
||||||
|
IsVisible = true
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace LocationsApi.Models;
|
||||||
|
|
||||||
|
public class LocationIntel
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
|
||||||
|
public string Username { get; set; } = "";
|
||||||
|
public int Q { get; set; }
|
||||||
|
public int R { get; set; }
|
||||||
|
public string Terrain { get; set; } = "Field";
|
||||||
|
public string Kind { get; set; } = "Open";
|
||||||
|
public string? OwnerUsername { get; set; }
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public int Level { get; set; } = 1;
|
||||||
|
public string? BuildingType { get; set; }
|
||||||
|
public string? BuildingStatus { get; set; }
|
||||||
|
public DateTime? BuildingStartedAtUtc { get; set; }
|
||||||
|
public DateTime? BuildingCompletesAtUtc { get; set; }
|
||||||
|
public DateTime? BuildingBuiltAtUtc { get; set; }
|
||||||
|
public DateTime? HomeAssignedAtUtc { get; set; }
|
||||||
|
public DateTime ObservedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ namespace LocationsApi.Services;
|
|||||||
public class LocationService
|
public class LocationService
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<Location> _locations;
|
private readonly IMongoCollection<Location> _locations;
|
||||||
|
private readonly IMongoCollection<UnitInstance> _units;
|
||||||
|
private readonly IMongoCollection<LocationIntel> _intel;
|
||||||
private readonly ResourceService _resources;
|
private readonly ResourceService _resources;
|
||||||
private readonly GameSettingsService _settings;
|
private readonly GameSettingsService _settings;
|
||||||
private readonly int _homeMinDistance;
|
private readonly int _homeMinDistance;
|
||||||
@@ -27,6 +29,8 @@ public class LocationService
|
|||||||
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");
|
||||||
|
_units = database.GetCollection<UnitInstance>("Units");
|
||||||
|
_intel = database.GetCollection<LocationIntel>("LocationIntel");
|
||||||
_resources = resources;
|
_resources = resources;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_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;
|
||||||
@@ -74,6 +78,13 @@ public class LocationService
|
|||||||
Unique = true,
|
Unique = true,
|
||||||
PartialFilterExpression = Builders<Location>.Filter.Eq(x => x.Kind, "Home")
|
PartialFilterExpression = Builders<Location>.Filter.Eq(x => x.Kind, "Home")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
_intel.Indexes.CreateOne(new CreateIndexModel<LocationIntel>(
|
||||||
|
Builders<LocationIntel>.IndexKeys
|
||||||
|
.Ascending(x => x.Username)
|
||||||
|
.Ascending(x => x.Q)
|
||||||
|
.Ascending(x => x.R),
|
||||||
|
new CreateIndexOptions { Unique = true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<Location>> GetAllAsync() =>
|
public Task<List<Location>> GetAllAsync() =>
|
||||||
@@ -315,8 +326,10 @@ public class LocationService
|
|||||||
ownedLocations.Add(await AssignHomeAsync(username));
|
ownedLocations.Add(await AssignHomeAsync(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var playerUnits = await _units.Find(x => x.Username == username).ToListAsync();
|
||||||
var depth = Math.Max(0, visibilityDepth ?? _visibilityDepth);
|
var depth = Math.Max(0, visibilityDepth ?? _visibilityDepth);
|
||||||
var visibleCoordinates = new HashSet<(int q, int r)>();
|
var visibleCoordinates = new HashSet<(int q, int r)>();
|
||||||
|
var occupiedCoordinates = playerUnits.Select(x => (x.Q, x.R)).ToHashSet();
|
||||||
|
|
||||||
foreach (var owned in ownedLocations)
|
foreach (var owned in ownedLocations)
|
||||||
{
|
{
|
||||||
@@ -326,6 +339,14 @@ public class LocationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var unit in playerUnits)
|
||||||
|
{
|
||||||
|
foreach (var coordinate in HexRange(unit.Q, unit.R, 1))
|
||||||
|
{
|
||||||
|
visibleCoordinates.Add(coordinate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var minQ = visibleCoordinates.Min(x => x.q);
|
var minQ = visibleCoordinates.Min(x => x.q);
|
||||||
var maxQ = visibleCoordinates.Max(x => x.q);
|
var maxQ = visibleCoordinates.Max(x => x.q);
|
||||||
var minR = visibleCoordinates.Min(x => x.r);
|
var minR = visibleCoordinates.Min(x => x.r);
|
||||||
@@ -349,12 +370,15 @@ public class LocationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await RefreshIntelAsync(username, occupiedCoordinates, existingByCoordinate);
|
||||||
|
var intelByCoordinate = await GetIntelByCoordinateAsync(username, visibleCoordinates);
|
||||||
|
|
||||||
return visibleCoordinates
|
return visibleCoordinates
|
||||||
.OrderBy(x => x.q)
|
.OrderBy(x => x.q)
|
||||||
.ThenBy(x => x.r)
|
.ThenBy(x => x.r)
|
||||||
.Select(coordinate =>
|
.Select(coordinate =>
|
||||||
existingByCoordinate.TryGetValue(coordinate, out var existing)
|
existingByCoordinate.TryGetValue(coordinate, out var existing)
|
||||||
? LocationResponse.FromLocation(existing)
|
? VisibleResponseFor(username, existing, occupiedCoordinates, intelByCoordinate)
|
||||||
: throw new InvalidOperationException($"Visible location {coordinate.q},{coordinate.r} was not generated."))
|
: throw new InvalidOperationException($"Visible location {coordinate.q},{coordinate.r} was not generated."))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -369,7 +393,10 @@ public class LocationService
|
|||||||
var existing = await GetAsync(q, r);
|
var existing = await GetAsync(q, r);
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
{
|
{
|
||||||
return LocationResponse.FromLocation(existing);
|
var occupiedCoordinates = await GetOccupiedCoordinatesAsync(username);
|
||||||
|
await RefreshIntelAsync(username, occupiedCoordinates, new Dictionary<(int Q, int R), Location> { [(existing.Q, existing.R)] = existing });
|
||||||
|
var intelByCoordinate = await GetIntelByCoordinateAsync(username, new[] { (q, r) });
|
||||||
|
return VisibleResponseFor(username, existing, occupiedCoordinates, intelByCoordinate);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nearbyLocations = await GetRangeAsync(q - 1, q + 1, r - 1, r + 1);
|
var nearbyLocations = await GetRangeAsync(q - 1, q + 1, r - 1, r + 1);
|
||||||
@@ -388,7 +415,13 @@ public class LocationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth);
|
if (ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerUnits = await _units.Find(x => x.Username == username).ToListAsync();
|
||||||
|
return playerUnits.Any(unit => HexDistance(unit.Q, unit.R, q, r) <= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsAdjacentToOwnedAsync(string username, int q, int r)
|
public async Task<bool> IsAdjacentToOwnedAsync(string username, int q, int r)
|
||||||
@@ -434,6 +467,101 @@ public class LocationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<(int Q, int R)>> GetOccupiedCoordinatesAsync(string username)
|
||||||
|
{
|
||||||
|
var units = await _units.Find(x => x.Username == username).ToListAsync();
|
||||||
|
return units.Select(x => (x.Q, x.R)).ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<(int Q, int R), LocationIntel>> GetIntelByCoordinateAsync(
|
||||||
|
string username,
|
||||||
|
IEnumerable<(int q, int r)> coordinates)
|
||||||
|
{
|
||||||
|
var coordinateList = coordinates.Select(x => (x.q, x.r)).ToList();
|
||||||
|
if (coordinateList.Count == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<(int Q, int R), LocationIntel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var minQ = coordinateList.Min(x => x.q);
|
||||||
|
var maxQ = coordinateList.Max(x => x.q);
|
||||||
|
var minR = coordinateList.Min(x => x.r);
|
||||||
|
var maxR = coordinateList.Max(x => x.r);
|
||||||
|
var coordinateSet = coordinateList.ToHashSet();
|
||||||
|
var records = await _intel.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.Q >= minQ && x.Q <= maxQ &&
|
||||||
|
x.R >= minR && x.R <= maxR)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return records
|
||||||
|
.Where(x => coordinateSet.Contains((x.Q, x.R)))
|
||||||
|
.ToDictionary(x => (x.Q, x.R), x => x);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshIntelAsync(
|
||||||
|
string username,
|
||||||
|
HashSet<(int Q, int R)> occupiedCoordinates,
|
||||||
|
Dictionary<(int Q, int R), Location> existingByCoordinate)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var coordinate in occupiedCoordinates)
|
||||||
|
{
|
||||||
|
if (!existingByCoordinate.TryGetValue(coordinate, out var location))
|
||||||
|
{
|
||||||
|
location = await GetAsync(coordinate.Q, coordinate.R);
|
||||||
|
if (location is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var update = Builders<LocationIntel>.Update
|
||||||
|
.SetOnInsert(x => x.CreatedAtUtc, now)
|
||||||
|
.Set(x => x.Terrain, location.Terrain)
|
||||||
|
.Set(x => x.Kind, location.Kind)
|
||||||
|
.Set(x => x.OwnerUsername, location.OwnerUsername)
|
||||||
|
.Set(x => x.Label, location.Label)
|
||||||
|
.Set(x => x.Level, location.Level)
|
||||||
|
.Set(x => x.BuildingType, location.BuildingType)
|
||||||
|
.Set(x => x.BuildingStatus, location.BuildingStatus)
|
||||||
|
.Set(x => x.BuildingStartedAtUtc, location.BuildingStartedAtUtc)
|
||||||
|
.Set(x => x.BuildingCompletesAtUtc, location.BuildingCompletesAtUtc)
|
||||||
|
.Set(x => x.BuildingBuiltAtUtc, location.BuildingBuiltAtUtc)
|
||||||
|
.Set(x => x.HomeAssignedAtUtc, location.HomeAssignedAtUtc)
|
||||||
|
.Set(x => x.ObservedAtUtc, now)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
|
await _intel.UpdateOneAsync(
|
||||||
|
x => x.Username == username && x.Q == coordinate.Q && x.R == coordinate.R,
|
||||||
|
update,
|
||||||
|
new UpdateOptions { IsUpsert = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocationResponse VisibleResponseFor(
|
||||||
|
string username,
|
||||||
|
Location location,
|
||||||
|
HashSet<(int Q, int R)> occupiedCoordinates,
|
||||||
|
Dictionary<(int Q, int R), LocationIntel> intelByCoordinate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(location.OwnerUsername) ||
|
||||||
|
string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
occupiedCoordinates.Contains((location.Q, location.R)))
|
||||||
|
{
|
||||||
|
return LocationResponse.FromLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
return intelByCoordinate.TryGetValue((location.Q, location.R), out var intel)
|
||||||
|
? LocationResponse.FromIntel(intel)
|
||||||
|
: LocationResponse.FromLocation(location, revealDetails: false);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<List<Location>> GenerateVisibleLocationsAsync(
|
private async Task<List<Location>> GenerateVisibleLocationsAsync(
|
||||||
List<(int q, int r)> missingCoordinates,
|
List<(int q, int r)> missingCoordinates,
|
||||||
Dictionary<(int Q, int R), Location> existingByCoordinate)
|
Dictionary<(int Q, int R), Location> existingByCoordinate)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ public class UnitTrainingService
|
|||||||
|
|
||||||
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
|
if (!await CanMoveToAsync(username, request.ToQ, request.ToR))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Archers can only move to visible non-rival tiles.");
|
throw new InvalidOperationException("Archers can only move to visible tiles.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -187,6 +187,8 @@ public class UnitTrainingService
|
|||||||
.Set(x => x.Status, "Idle")
|
.Set(x => x.Status, "Idle")
|
||||||
.Set(x => x.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
|
||||||
|
await ClaimTargetIfEmptyAsync(username, request.ToQ, request.ToR, now);
|
||||||
|
|
||||||
return await _units.Find(x =>
|
return await _units.Find(x =>
|
||||||
x.Username == username &&
|
x.Username == username &&
|
||||||
x.UnitType == ArcherUnitType &&
|
x.UnitType == ArcherUnitType &&
|
||||||
@@ -226,15 +228,62 @@ public class UnitTrainingService
|
|||||||
private async Task<bool> CanMoveToAsync(string username, int q, int r)
|
private async Task<bool> CanMoveToAsync(string username, int q, int r)
|
||||||
{
|
{
|
||||||
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||||
if (!ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth))
|
if (ownedLocations.Any(owned => HexDistance(owned.Q, owned.R, q, r) <= _visibilityDepth))
|
||||||
{
|
{
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var playerUnits = await _units.Find(x => x.Username == username).ToListAsync();
|
||||||
|
return playerUnits.Any(unit => HexDistance(unit.Q, unit.R, q, r) <= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClaimTargetIfEmptyAsync(string username, int q, int r, DateTime now)
|
||||||
|
{
|
||||||
|
var hasOtherUnits = await _units.Find(x => x.Username != username && x.Q == q && x.R == r).AnyAsync();
|
||||||
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
|
var target = await _locations.Find(x => x.Q == q && x.R == r).FirstOrDefaultAsync();
|
||||||
return target is null ||
|
var hasBuilding = target is not null && !string.IsNullOrWhiteSpace(target.BuildingType);
|
||||||
string.IsNullOrWhiteSpace(target.OwnerUsername) ||
|
|
||||||
string.Equals(target.OwnerUsername, username, StringComparison.OrdinalIgnoreCase);
|
if (hasOtherUnits || hasBuilding)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
var location = new Location
|
||||||
|
{
|
||||||
|
Q = q,
|
||||||
|
R = r,
|
||||||
|
Terrain = "Field",
|
||||||
|
Kind = "Claimed",
|
||||||
|
OwnerUsername = username,
|
||||||
|
Level = 1,
|
||||||
|
CreatedAtUtc = now,
|
||||||
|
UpdatedAtUtc = now
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _locations.InsertOneAsync(location);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(target.OwnerUsername, username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _locations.UpdateOneAsync(
|
||||||
|
x => x.Id == target.Id && (x.BuildingType == null || x.BuildingType == ""),
|
||||||
|
Builders<Location>.Update
|
||||||
|
.Set(x => x.OwnerUsername, username)
|
||||||
|
.Set(x => x.Kind, "Claimed")
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int HexDistance(int q1, int r1, int q2, int r2)
|
private static int HexDistance(int q1, int r1, int q2, int r2)
|
||||||
|
|||||||
Reference in New Issue
Block a user