Updating movement
This commit is contained in:
@@ -27,7 +27,7 @@ var refresh_dialog: ConfirmationDialog
|
|||||||
var session: Dictionary = {}
|
var session: Dictionary = {}
|
||||||
var escape_overlay: Control
|
var escape_overlay: Control
|
||||||
var escape_panel: PanelContainer
|
var escape_panel: PanelContainer
|
||||||
var gold_label: Label
|
var gold_label: RichTextLabel
|
||||||
var tick_bar: ProgressBar
|
var tick_bar: ProgressBar
|
||||||
var tick_label: Label
|
var tick_label: Label
|
||||||
var resource_text := "Gold: --\nWood: --\nOre: --\nFood: --"
|
var resource_text := "Gold: --\nWood: --\nOre: --\nFood: --"
|
||||||
@@ -357,10 +357,14 @@ func _build_map_overlays(map_layer: Control) -> void:
|
|||||||
resource_stack.add_theme_constant_override("separation", 6)
|
resource_stack.add_theme_constant_override("separation", 6)
|
||||||
resource_margin.add_child(resource_stack)
|
resource_margin.add_child(resource_stack)
|
||||||
|
|
||||||
gold_label = Label.new()
|
gold_label = RichTextLabel.new()
|
||||||
gold_label.text = resource_text
|
gold_label.text = resource_text
|
||||||
|
gold_label.bbcode_enabled = true
|
||||||
|
gold_label.fit_content = true
|
||||||
|
gold_label.scroll_active = false
|
||||||
|
gold_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
gold_label.custom_minimum_size = Vector2(190, 74)
|
||||||
gold_label.add_theme_font_size_override("font_size", 14)
|
gold_label.add_theme_font_size_override("font_size", 14)
|
||||||
gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32))
|
|
||||||
resource_stack.add_child(gold_label)
|
resource_stack.add_child(gold_label)
|
||||||
|
|
||||||
tick_label = Label.new()
|
tick_label = Label.new()
|
||||||
@@ -539,7 +543,7 @@ func _handle_unit_selection_or_move(q: int, r: int) -> bool:
|
|||||||
selected_peasant_count = 0
|
selected_peasant_count = 0
|
||||||
grid_view.set_selected_unit_tile(q, r)
|
grid_view.set_selected_unit_tile(q, r)
|
||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Selected Archer squad: %d at q:%d r:%d. Right-click an adjacent visible tile for orders." % [archer_count, q, r]
|
coordinate_label.text = "Selected Archer squad: %d at q:%d r:%d. Right-click a visible tile for orders." % [archer_count, q, r]
|
||||||
return true
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -562,10 +566,6 @@ func _on_tile_action_requested(q: int, r: int, _terrain: String, screen_position
|
|||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Archer squad is already on that tile."
|
coordinate_label.text = "Archer squad is already on that tile."
|
||||||
return
|
return
|
||||||
if selected_archer_count > 0 and not grid_view.is_neighbor(selected_archer_q, selected_archer_r, q, r):
|
|
||||||
if coordinate_label:
|
|
||||||
coordinate_label.text = "Archers can only move to an adjacent tile."
|
|
||||||
return
|
|
||||||
if selected_peasants and not grid_view.is_owned_tile(q, r):
|
if selected_peasants and not grid_view.is_owned_tile(q, r):
|
||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Peasants can only move within controlled territory."
|
coordinate_label.text = "Peasants can only move within controlled territory."
|
||||||
@@ -623,7 +623,8 @@ func _location_has_completed_building(location: Dictionary) -> bool:
|
|||||||
return false
|
return false
|
||||||
var building_type: String = _nullable_string(location.get("buildingType", ""))
|
var building_type: String = _nullable_string(location.get("buildingType", ""))
|
||||||
var building_status: String = _nullable_string(location.get("buildingStatus", ""))
|
var building_status: String = _nullable_string(location.get("buildingStatus", ""))
|
||||||
return not building_type.is_empty() and (building_status == "Built" or building_status.is_empty())
|
var can_have_worker := building_type == "LogCamp" or building_type == "Mine" or building_type == "Farm"
|
||||||
|
return can_have_worker and (building_status == "Built" or building_status.is_empty())
|
||||||
|
|
||||||
|
|
||||||
func _can_build_log_camp_on_selected_tile() -> bool:
|
func _can_build_log_camp_on_selected_tile() -> bool:
|
||||||
@@ -1008,20 +1009,12 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
|
|||||||
var food: int = int(resources.get("food", 0))
|
var food: int = int(resources.get("food", 0))
|
||||||
var food_per_minute: int = int(resources.get("foodPerMinute", 0))
|
var food_per_minute: int = int(resources.get("foodPerMinute", 0))
|
||||||
var food_capacity: int = int(resources.get("foodCapacity", 100))
|
var food_capacity: int = int(resources.get("foodCapacity", 100))
|
||||||
resource_text = "Gold: %d/%d (+%d/min)\nWood: %d/%d (+%d/min)\nOre: %d/%d (+%d/min)\nFood: %d/%d (+%d/min)" % [
|
resource_text = "\n".join([
|
||||||
gold,
|
_resource_line("Gold", gold, gold_capacity, gold_per_minute),
|
||||||
gold_capacity,
|
_resource_line("Wood", wood, wood_capacity, wood_per_minute),
|
||||||
gold_per_minute,
|
_resource_line("Ore", ore, ore_capacity, ore_per_minute),
|
||||||
wood,
|
_resource_line("Food", food, food_capacity, food_per_minute)
|
||||||
wood_capacity,
|
])
|
||||||
wood_per_minute,
|
|
||||||
ore,
|
|
||||||
ore_capacity,
|
|
||||||
ore_per_minute,
|
|
||||||
food,
|
|
||||||
food_capacity,
|
|
||||||
food_per_minute
|
|
||||||
]
|
|
||||||
gold_label.text = resource_text
|
gold_label.text = resource_text
|
||||||
|
|
||||||
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
|
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
|
||||||
@@ -1031,6 +1024,27 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
|
|||||||
_update_tick_bar()
|
_update_tick_bar()
|
||||||
|
|
||||||
|
|
||||||
|
func _resource_line(resource_name: String, amount: int, capacity: int, rate: int) -> String:
|
||||||
|
var value_color := "#f2c752"
|
||||||
|
var rate_color := "#d9574f" if rate < 0 else "#f2c752"
|
||||||
|
return "[color=%s]%s: %d/%d[/color] [color=%s](%s)[/color]" % [
|
||||||
|
value_color,
|
||||||
|
resource_name,
|
||||||
|
amount,
|
||||||
|
capacity,
|
||||||
|
rate_color,
|
||||||
|
_resource_rate_text(rate)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _resource_rate_text(rate: int) -> String:
|
||||||
|
if rate > 0:
|
||||||
|
return "+%d/min" % rate
|
||||||
|
if rate < 0:
|
||||||
|
return "%d/min" % rate
|
||||||
|
return "0/min"
|
||||||
|
|
||||||
|
|
||||||
func _load_units() -> void:
|
func _load_units() -> void:
|
||||||
var access_token: String = str(session.get("accessToken", ""))
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
if access_token.is_empty():
|
if access_token.is_empty():
|
||||||
@@ -1183,9 +1197,6 @@ func _move_selected_archers(to_q: int, to_r: int) -> void:
|
|||||||
if to_q == selected_archer_q and to_r == selected_archer_r:
|
if to_q == selected_archer_q and to_r == selected_archer_r:
|
||||||
coordinate_label.text = "Archer squad is already on that tile."
|
coordinate_label.text = "Archer squad is already on that tile."
|
||||||
return
|
return
|
||||||
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."
|
|
||||||
return
|
|
||||||
if not grid_view.is_visible_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 tiles."
|
coordinate_label.text = "Archers can only move to visible tiles."
|
||||||
return
|
return
|
||||||
@@ -1267,6 +1278,10 @@ func _on_move_units_request_completed(result: int, response_code: int, _headers:
|
|||||||
_refresh_player_state()
|
_refresh_player_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
var parsed = JSON.parse_string(text)
|
||||||
|
if typeof(parsed) == TYPE_ARRAY and grid_view:
|
||||||
|
grid_view.upsert_unit_instances(parsed)
|
||||||
|
|
||||||
_clear_selected_archers()
|
_clear_selected_archers()
|
||||||
coordinate_label.text = "Unit movement ordered."
|
coordinate_label.text = "Unit movement ordered."
|
||||||
_load_visible_locations()
|
_load_visible_locations()
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ func _draw_tile(tile: Dictionary) -> void:
|
|||||||
var archer_count := _archer_count_at(q, r)
|
var archer_count := _archer_count_at(q, r)
|
||||||
if archer_count > 0:
|
if archer_count > 0:
|
||||||
_draw_archer_unit_marker(center, height, archer_count)
|
_draw_archer_unit_marker(center, height, archer_count)
|
||||||
|
var scout_count := _scout_count_at(q, r)
|
||||||
|
if scout_count > 0:
|
||||||
|
_draw_scout_unit_marker(center, height, scout_count)
|
||||||
var peasant_count := _peasant_count_at(q, r)
|
var peasant_count := _peasant_count_at(q, r)
|
||||||
if peasant_count > 0:
|
if peasant_count > 0:
|
||||||
_draw_peasant_unit_marker(center, height, peasant_count)
|
_draw_peasant_unit_marker(center, height, peasant_count)
|
||||||
@@ -622,6 +625,29 @@ func _draw_peasant_unit_marker(center: Vector2, height: float, count: int) -> vo
|
|||||||
draw_string(ThemeDB.fallback_font, marker_center + Vector2(7, 10) * marker_scale, str(count), HORIZONTAL_ALIGNMENT_LEFT, -1, int(9 * marker_scale), Color(0.94, 0.86, 0.6))
|
draw_string(ThemeDB.fallback_font, marker_center + Vector2(7, 10) * marker_scale, str(count), HORIZONTAL_ALIGNMENT_LEFT, -1, int(9 * marker_scale), Color(0.94, 0.86, 0.6))
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_scout_unit_marker(center: Vector2, height: float, count: int) -> void:
|
||||||
|
var marker_center: Vector2 = center + Vector2(-18, -height * zoom - 12.0) * zoom
|
||||||
|
var marker_scale: float = zoom
|
||||||
|
var cloak_color := Color(0.46, 0.38, 0.20)
|
||||||
|
var hood_color := Color(0.68, 0.52, 0.24)
|
||||||
|
var outline_color := Color(0.08, 0.06, 0.03)
|
||||||
|
var glass_color := Color(0.92, 0.78, 0.34)
|
||||||
|
|
||||||
|
draw_circle(marker_center + Vector2(1, 2) * marker_scale, 8.5 * marker_scale, Color(0.01, 0.02, 0.02, 0.45))
|
||||||
|
var body := PackedVector2Array([
|
||||||
|
marker_center + Vector2(-6, 8) * marker_scale,
|
||||||
|
marker_center + Vector2(0, -8) * marker_scale,
|
||||||
|
marker_center + Vector2(6, 8) * marker_scale
|
||||||
|
])
|
||||||
|
draw_colored_polygon(body, cloak_color)
|
||||||
|
draw_polyline(_closed_points(body), outline_color, 1.1 * marker_scale)
|
||||||
|
draw_circle(marker_center + Vector2(0, -6) * marker_scale, 4.0 * marker_scale, hood_color)
|
||||||
|
draw_line(marker_center + Vector2(5, -5) * marker_scale, marker_center + Vector2(12, -11) * marker_scale, glass_color, 2.0 * marker_scale)
|
||||||
|
draw_circle(marker_center + Vector2(13, -12) * marker_scale, 3.0 * marker_scale, glass_color)
|
||||||
|
if count > 1:
|
||||||
|
draw_string(ThemeDB.fallback_font, marker_center + Vector2(7, 10) * marker_scale, str(count), HORIZONTAL_ALIGNMENT_LEFT, -1, int(9 * marker_scale), Color(0.94, 0.86, 0.6))
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -699,6 +725,23 @@ func set_unit_instances(units: Array) -> void:
|
|||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func upsert_unit_instances(units: Array) -> void:
|
||||||
|
for item in units:
|
||||||
|
if typeof(item) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var unit: Dictionary = item
|
||||||
|
var unit_id := str(unit.get("id", ""))
|
||||||
|
var replaced := false
|
||||||
|
for index in range(unit_instances.size()):
|
||||||
|
if str(unit_instances[index].get("id", "")) == unit_id:
|
||||||
|
unit_instances[index] = unit
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
if not replaced:
|
||||||
|
unit_instances.append(unit)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func set_selected_unit_tile(q: int, r: int) -> void:
|
func set_selected_unit_tile(q: int, r: int) -> void:
|
||||||
selected_unit_tile = Vector2i(q, r)
|
selected_unit_tile = Vector2i(q, r)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
@@ -853,11 +896,21 @@ func _archer_count_at(q: int, r: int) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func _scout_count_at(q: int, r: int) -> int:
|
||||||
|
var count := 0
|
||||||
|
for unit in unit_instances:
|
||||||
|
if str(unit.get("unitType", "")) != "Scout":
|
||||||
|
continue
|
||||||
|
if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
func _peasant_count_at(q: int, r: int) -> int:
|
func _peasant_count_at(q: int, r: int) -> int:
|
||||||
var count := 0
|
var count := 0
|
||||||
for unit in unit_instances:
|
for unit in unit_instances:
|
||||||
var status := str(unit.get("status", ""))
|
var status := str(unit.get("status", ""))
|
||||||
if str(unit.get("unitType", "")) != "Peasant" or (status != "Idle" and status != "MovingToWork"):
|
if str(unit.get("unitType", "")) != "Peasant" or (status != "Idle" and status != "Moving" and status != "MovingToWork"):
|
||||||
continue
|
continue
|
||||||
if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r:
|
if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r:
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ public class LocationService
|
|||||||
var location = await GetAsync(request.Q, request.R);
|
var location = await GetAsync(request.Q, request.R);
|
||||||
if (location is null ||
|
if (location is null ||
|
||||||
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) ||
|
!string.Equals(location.OwnerUsername, username, StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.IsNullOrWhiteSpace(location.BuildingType) ||
|
!CanAssignWorkerToBuilding(location.BuildingType) ||
|
||||||
location.BuildingStatus == "Building" ||
|
location.BuildingStatus == "Building" ||
|
||||||
!string.IsNullOrWhiteSpace(location.WorkerUnitId))
|
!string.IsNullOrWhiteSpace(location.WorkerUnitId))
|
||||||
{
|
{
|
||||||
@@ -314,7 +314,7 @@ public class LocationService
|
|||||||
.Set(x => x.DestinationQ, request.Q)
|
.Set(x => x.DestinationQ, request.Q)
|
||||||
.Set(x => x.DestinationR, request.R)
|
.Set(x => x.DestinationR, request.R)
|
||||||
.Set(x => x.MovementStartedAtUtc, now)
|
.Set(x => x.MovementStartedAtUtc, now)
|
||||||
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(Math.Max(1, HexDistance(unit.Q, unit.R, request.Q, request.R))))
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
||||||
.Set(x => x.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
if (claimed.ModifiedCount == 0)
|
if (claimed.ModifiedCount == 0)
|
||||||
{
|
{
|
||||||
@@ -535,23 +535,29 @@ public class LocationService
|
|||||||
|
|
||||||
var destinationQ = worker.DestinationQ.Value;
|
var destinationQ = worker.DestinationQ.Value;
|
||||||
var destinationR = worker.DestinationR.Value;
|
var destinationR = worker.DestinationR.Value;
|
||||||
var currentQ = worker.Q;
|
if (HexDistance(worker.Q, worker.R, destinationQ, destinationR) > 1)
|
||||||
var currentR = worker.R;
|
|
||||||
var nextTick = worker.MovementCompletesAtUtc!.Value;
|
|
||||||
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
{
|
||||||
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
|
var nextStep = await NextPeasantStepTowardAsync(username, worker.Q, worker.R, destinationQ, destinationR);
|
||||||
nextTick = nextTick.AddMinutes(1);
|
if (nextStep is null)
|
||||||
}
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
|
||||||
await _units.UpdateOneAsync(
|
await _units.UpdateOneAsync(
|
||||||
x => x.Id == worker.Id,
|
x => x.Id == worker.Id,
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
.Set(x => x.Q, currentQ)
|
.Set(x => x.Q, nextStep.Value.q)
|
||||||
.Set(x => x.R, currentR)
|
.Set(x => x.R, nextStep.Value.r)
|
||||||
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
||||||
.Set(x => x.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -560,7 +566,7 @@ public class LocationService
|
|||||||
x => x.OwnerUsername == username &&
|
x => x.OwnerUsername == username &&
|
||||||
x.Q == destinationQ &&
|
x.Q == destinationQ &&
|
||||||
x.R == destinationR &&
|
x.R == destinationR &&
|
||||||
!string.IsNullOrWhiteSpace(x.BuildingType) &&
|
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Farm") &&
|
||||||
x.BuildingStatus != "Building" &&
|
x.BuildingStatus != "Building" &&
|
||||||
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||||
Builders<Location>.Update
|
Builders<Location>.Update
|
||||||
@@ -591,13 +597,63 @@ public class LocationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
private async Task<(int q, int r)?> NextPeasantStepTowardAsync(string username, int fromQ, int fromR, int toQ, int toR)
|
||||||
{
|
{
|
||||||
return NeighborCoordinates(fromQ, fromR)
|
var owned = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||||
.OrderBy(x => HexDistance(x.Q, x.R, toQ, toR))
|
var ownedCoordinates = owned.Select(x => (x.Q, x.R)).ToHashSet();
|
||||||
.ThenBy(x => x.Q)
|
if (!ownedCoordinates.Contains((toQ, toR)))
|
||||||
.ThenBy(x => x.R)
|
{
|
||||||
.First();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = (fromQ, fromR);
|
||||||
|
var target = (toQ, toR);
|
||||||
|
var queue = new Queue<(int q, int r)>();
|
||||||
|
var previous = new Dictionary<(int q, int r), (int q, int r)>();
|
||||||
|
var visited = new HashSet<(int q, int r)> { start };
|
||||||
|
queue.Enqueue(start);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var current = queue.Dequeue();
|
||||||
|
if (current == target)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var neighbor in NeighborCoordinates(current.q, current.r)
|
||||||
|
.Select(x => (x.Q, x.R))
|
||||||
|
.OrderBy(x => HexDistance(x.Q, x.R, toQ, toR))
|
||||||
|
.ThenBy(x => x.Q)
|
||||||
|
.ThenBy(x => x.R))
|
||||||
|
{
|
||||||
|
if (!ownedCoordinates.Contains(neighbor) || !visited.Add(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous[neighbor] = current;
|
||||||
|
queue.Enqueue(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visited.Contains(target))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var step = target;
|
||||||
|
while (previous.TryGetValue(step, out var before) && before != start)
|
||||||
|
{
|
||||||
|
step = before;
|
||||||
|
}
|
||||||
|
|
||||||
|
return step == start ? target : step;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanAssignWorkerToBuilding(string? buildingType)
|
||||||
|
{
|
||||||
|
return buildingType == "LogCamp" || buildingType == "Mine" || buildingType == "Farm";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanSeeAsync(string username, int q, int r)
|
public async Task<bool> CanSeeAsync(string username, int q, int r)
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ public class ResourceService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var persistedUnits = await _units.Find(x =>
|
var persistedUnits = await _units.Find(x =>
|
||||||
x.Username == username &&
|
x.Username == username &&
|
||||||
(x.UnitType == ArcherUnitType || x.UnitType == PeasantUnitType))
|
(x.UnitType == ScoutUnitType || x.UnitType == ArcherUnitType || x.UnitType == PeasantUnitType))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var units = completedScouts
|
var units = completedScouts
|
||||||
@@ -511,23 +511,29 @@ public class ResourceService
|
|||||||
|
|
||||||
var destinationQ = worker.DestinationQ.Value;
|
var destinationQ = worker.DestinationQ.Value;
|
||||||
var destinationR = worker.DestinationR.Value;
|
var destinationR = worker.DestinationR.Value;
|
||||||
var currentQ = worker.Q;
|
if (HexDistance(worker.Q, worker.R, destinationQ, destinationR) > 1)
|
||||||
var currentR = worker.R;
|
|
||||||
var nextTick = worker.MovementCompletesAtUtc!.Value;
|
|
||||||
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
{
|
||||||
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
|
var nextStep = await NextPeasantStepTowardAsync(username, worker.Q, worker.R, destinationQ, destinationR);
|
||||||
nextTick = nextTick.AddMinutes(1);
|
if (nextStep is null)
|
||||||
}
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
|
||||||
await _units.UpdateOneAsync(
|
await _units.UpdateOneAsync(
|
||||||
x => x.Id == worker.Id,
|
x => x.Id == worker.Id,
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
.Set(x => x.Q, currentQ)
|
.Set(x => x.Q, nextStep.Value.q)
|
||||||
.Set(x => x.R, currentR)
|
.Set(x => x.R, nextStep.Value.r)
|
||||||
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
||||||
.Set(x => x.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -536,7 +542,7 @@ public class ResourceService
|
|||||||
x => x.OwnerUsername == username &&
|
x => x.OwnerUsername == username &&
|
||||||
x.Q == destinationQ &&
|
x.Q == destinationQ &&
|
||||||
x.R == destinationR &&
|
x.R == destinationR &&
|
||||||
!string.IsNullOrWhiteSpace(x.BuildingType) &&
|
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || x.BuildingType == "Farm") &&
|
||||||
x.BuildingStatus != "Building" &&
|
x.BuildingStatus != "Building" &&
|
||||||
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||||
Builders<Location>.Update
|
Builders<Location>.Update
|
||||||
@@ -561,13 +567,57 @@ public class ResourceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
private async Task<(int q, int r)?> NextPeasantStepTowardAsync(string username, int fromQ, int fromR, int toQ, int toR)
|
||||||
{
|
{
|
||||||
return NeighborCoordinates(fromQ, fromR)
|
var owned = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||||
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
var ownedCoordinates = owned.Select(x => (x.Q, x.R)).ToHashSet();
|
||||||
.ThenBy(x => x.q)
|
if (!ownedCoordinates.Contains((toQ, toR)))
|
||||||
.ThenBy(x => x.r)
|
{
|
||||||
.First();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = (fromQ, fromR);
|
||||||
|
var target = (toQ, toR);
|
||||||
|
var queue = new Queue<(int q, int r)>();
|
||||||
|
var previous = new Dictionary<(int q, int r), (int q, int r)>();
|
||||||
|
var visited = new HashSet<(int q, int r)> { start };
|
||||||
|
queue.Enqueue(start);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var current = queue.Dequeue();
|
||||||
|
if (current == target)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var neighbor in NeighborCoordinates(current.q, current.r)
|
||||||
|
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
||||||
|
.ThenBy(x => x.q)
|
||||||
|
.ThenBy(x => x.r))
|
||||||
|
{
|
||||||
|
if (!ownedCoordinates.Contains(neighbor) || !visited.Add(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous[neighbor] = current;
|
||||||
|
queue.Enqueue(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visited.Contains(target))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var step = target;
|
||||||
|
while (previous.TryGetValue(step, out var before) && before != start)
|
||||||
|
{
|
||||||
|
step = before;
|
||||||
|
}
|
||||||
|
|
||||||
|
return step == start ? target : step;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
|
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
|
||||||
|
|||||||
@@ -69,13 +69,14 @@ public class UnitTrainingService
|
|||||||
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
||||||
{
|
{
|
||||||
await CompleteFinishedTrainingAsync(username);
|
await CompleteFinishedTrainingAsync(username);
|
||||||
|
await PromoteCompletedScoutsAsync(username);
|
||||||
await CompletePendingMovementAsync(username);
|
await CompletePendingMovementAsync(username);
|
||||||
await RefillMovementAsync(username);
|
await RefillMovementAsync(username);
|
||||||
await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed");
|
await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed");
|
||||||
|
|
||||||
var allTraining = await _training.Find(x => x.Username == username).ToListAsync();
|
var allTraining = await _training.Find(x => x.Username == username).ToListAsync();
|
||||||
var allUnits = await _units.Find(x => x.Username == username).ToListAsync();
|
var allUnits = await _units.Find(x => x.Username == username).ToListAsync();
|
||||||
var completedScouts = allTraining.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
|
var completedScouts = allUnits.Count(x => x.UnitType == ScoutUnitType && x.Status == "Idle");
|
||||||
var completedArchers = allUnits.Count(x => x.UnitType == ArcherUnitType);
|
var completedArchers = allUnits.Count(x => x.UnitType == ArcherUnitType);
|
||||||
var completedPeasants = allUnits.Count(x => x.UnitType == PeasantUnitType && x.Status == "Idle");
|
var completedPeasants = allUnits.Count(x => x.UnitType == PeasantUnitType && x.Status == "Idle");
|
||||||
var activeTraining = allTraining
|
var activeTraining = allTraining
|
||||||
@@ -129,6 +130,14 @@ public class UnitTrainingService
|
|||||||
throw new InvalidOperationException("A town hall is required to train Peasants.");
|
throw new InvalidOperationException("A town hall is required to train Peasants.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (normalizedUnitType == ScoutUnitType)
|
||||||
|
{
|
||||||
|
origin = await _locations.Find(x => x.OwnerUsername == username && x.Kind == "Home").FirstOrDefaultAsync();
|
||||||
|
if (origin is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A town hall is required to train Scouts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var goldCost = normalizedUnitType == ArcherUnitType
|
var goldCost = normalizedUnitType == ArcherUnitType
|
||||||
? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherGoldCost, _archerGoldCost)
|
? await _settings.GetNonNegativeIntAsync(GameSettingsService.ArcherGoldCost, _archerGoldCost)
|
||||||
@@ -188,11 +197,6 @@ public class UnitTrainingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR);
|
var movementDistance = HexDistance(request.FromQ, request.FromR, request.ToQ, request.ToR);
|
||||||
if (normalizedUnitType == ArcherUnitType && movementDistance != 1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Archers can only move to adjacent tiles.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedUnitType == ArcherUnitType && !await CanMoveToAsync(username, request.ToQ, request.ToR))
|
if (normalizedUnitType == ArcherUnitType && !await CanMoveToAsync(username, request.ToQ, request.ToR))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Archers can only move to visible tiles.");
|
throw new InvalidOperationException("Archers can only move to visible tiles.");
|
||||||
@@ -230,7 +234,7 @@ public class UnitTrainingService
|
|||||||
throw new InvalidOperationException("That squad has no movement remaining this tick.");
|
throw new InvalidOperationException("That squad has no movement remaining this tick.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var movementCompletesAt = now.AddMinutes(Math.Max(1, movementDistance));
|
var movementCompletesAt = now.AddMinutes(1);
|
||||||
await _units.UpdateManyAsync(
|
await _units.UpdateManyAsync(
|
||||||
unitFilter,
|
unitFilter,
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
@@ -294,23 +298,44 @@ public class UnitTrainingService
|
|||||||
|
|
||||||
var destinationQ = unit.DestinationQ.Value;
|
var destinationQ = unit.DestinationQ.Value;
|
||||||
var destinationR = unit.DestinationR.Value;
|
var destinationR = unit.DestinationR.Value;
|
||||||
var currentQ = unit.Q;
|
if (unit.UnitType == PeasantUnitType && HexDistance(unit.Q, unit.R, destinationQ, destinationR) > 1)
|
||||||
var currentR = unit.R;
|
|
||||||
var nextTick = unit.MovementCompletesAtUtc!.Value;
|
|
||||||
while (nextTick <= now && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
{
|
||||||
(currentQ, currentR) = NextStepToward(currentQ, currentR, destinationQ, destinationR);
|
var nextStep = await NextPeasantStepTowardAsync(username, unit.Q, unit.R, destinationQ, destinationR);
|
||||||
nextTick = nextTick.AddMinutes(1);
|
if (nextStep is null)
|
||||||
}
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == unit.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Set(x => x.MovementPerTick, movementPerTick)
|
||||||
|
.Set(x => x.MovementRemaining, movementPerTick)
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
|
||||||
{
|
|
||||||
await _units.UpdateOneAsync(
|
await _units.UpdateOneAsync(
|
||||||
x => x.Id == unit.Id,
|
x => x.Id == unit.Id,
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
.Set(x => x.Q, currentQ)
|
.Set(x => x.Q, nextStep.Value.q)
|
||||||
.Set(x => x.R, currentR)
|
.Set(x => x.R, nextStep.Value.r)
|
||||||
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit.UnitType != PeasantUnitType && HexDistance(unit.Q, unit.R, destinationQ, destinationR) > 1)
|
||||||
|
{
|
||||||
|
var nextStep = NextStepToward(unit.Q, unit.R, destinationQ, destinationR);
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == unit.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, nextStep.q)
|
||||||
|
.Set(x => x.R, nextStep.r)
|
||||||
|
.Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1))
|
||||||
.Set(x => x.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -405,6 +430,59 @@ public class UnitTrainingService
|
|||||||
.AnyAsync();
|
.AnyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(int q, int r)?> NextPeasantStepTowardAsync(string username, int fromQ, int fromR, int toQ, int toR)
|
||||||
|
{
|
||||||
|
var owned = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||||
|
var ownedCoordinates = owned.Select(x => (x.Q, x.R)).ToHashSet();
|
||||||
|
if (!ownedCoordinates.Contains((toQ, toR)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = (fromQ, fromR);
|
||||||
|
var target = (toQ, toR);
|
||||||
|
var queue = new Queue<(int q, int r)>();
|
||||||
|
var previous = new Dictionary<(int q, int r), (int q, int r)>();
|
||||||
|
var visited = new HashSet<(int q, int r)> { start };
|
||||||
|
queue.Enqueue(start);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var current = queue.Dequeue();
|
||||||
|
if (current == target)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var neighbor in NeighborCoordinates(current.q, current.r)
|
||||||
|
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
||||||
|
.ThenBy(x => x.q)
|
||||||
|
.ThenBy(x => x.r))
|
||||||
|
{
|
||||||
|
if (!ownedCoordinates.Contains(neighbor) || !visited.Add(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous[neighbor] = current;
|
||||||
|
queue.Enqueue(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visited.Contains(target))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var step = target;
|
||||||
|
while (previous.TryGetValue(step, out var before) && before != start)
|
||||||
|
{
|
||||||
|
step = before;
|
||||||
|
}
|
||||||
|
|
||||||
|
return step == start ? target : step;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ClaimTargetIfEmptyAsync(string username, int q, int r, DateTime now)
|
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 hasOtherUnits = await _units.Find(x => x.Username != username && x.Q == q && x.R == r).AnyAsync();
|
||||||
@@ -492,6 +570,18 @@ public class UnitTrainingService
|
|||||||
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
||||||
{
|
{
|
||||||
await CompleteFinishedTrainingAsync(username);
|
await CompleteFinishedTrainingAsync(username);
|
||||||
|
await PromoteCompletedScoutsAsync(username);
|
||||||
|
|
||||||
|
var consumedUnit = await _units.FindOneAndDeleteAsync(
|
||||||
|
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Idle",
|
||||||
|
new FindOneAndDeleteOptions<UnitInstance>
|
||||||
|
{
|
||||||
|
Sort = Builders<UnitInstance>.Sort.Ascending(x => x.CreatedAtUtc)
|
||||||
|
});
|
||||||
|
if (consumedUnit is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var consumed = await _training.FindOneAndDeleteAsync(
|
var consumed = await _training.FindOneAndDeleteAsync(
|
||||||
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
||||||
@@ -506,7 +596,8 @@ public class UnitTrainingService
|
|||||||
public async Task<bool> HasCompletedScoutAsync(string username)
|
public async Task<bool> HasCompletedScoutAsync(string username)
|
||||||
{
|
{
|
||||||
await CompleteFinishedTrainingAsync(username);
|
await CompleteFinishedTrainingAsync(username);
|
||||||
return await _training.Find(x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed").AnyAsync();
|
await PromoteCompletedScoutsAsync(username);
|
||||||
|
return await _units.Find(x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Idle").AnyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CompleteFinishedTrainingAsync(string username)
|
private async Task CompleteFinishedTrainingAsync(string username)
|
||||||
@@ -533,7 +624,7 @@ public class UnitTrainingService
|
|||||||
ReturnDocument = ReturnDocument.After
|
ReturnDocument = ReturnDocument.After
|
||||||
});
|
});
|
||||||
|
|
||||||
if (completed is null || (completed.UnitType != ArcherUnitType && completed.UnitType != PeasantUnitType))
|
if (completed is null || (completed.UnitType != ArcherUnitType && completed.UnitType != PeasantUnitType && completed.UnitType != ScoutUnitType))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -557,6 +648,14 @@ public class UnitTrainingService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _units.InsertOneAsync(unit);
|
await _units.InsertOneAsync(unit);
|
||||||
|
if (completed.UnitType == ScoutUnitType)
|
||||||
|
{
|
||||||
|
await _training.UpdateOneAsync(
|
||||||
|
x => x.Id == completed.Id,
|
||||||
|
Builders<UnitTraining>.Update
|
||||||
|
.Set(x => x.Status, "Consumed")
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (MongoWriteException)
|
catch (MongoWriteException)
|
||||||
{
|
{
|
||||||
@@ -564,6 +663,58 @@ public class UnitTrainingService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PromoteCompletedScoutsAsync(string username)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var completedScouts = await _training.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.UnitType == ScoutUnitType &&
|
||||||
|
x.Status == "Completed")
|
||||||
|
.ToListAsync();
|
||||||
|
if (completedScouts.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var home = await _locations.Find(x => x.OwnerUsername == username && x.Kind == "Home").FirstOrDefaultAsync();
|
||||||
|
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||||
|
foreach (var scout in completedScouts)
|
||||||
|
{
|
||||||
|
var existing = await _units.Find(x => x.SourceTrainingId == scout.Id).AnyAsync();
|
||||||
|
if (!existing)
|
||||||
|
{
|
||||||
|
var unit = new UnitInstance
|
||||||
|
{
|
||||||
|
Username = scout.Username,
|
||||||
|
UnitType = ScoutUnitType,
|
||||||
|
Q = scout.OriginQ ?? home?.Q ?? 0,
|
||||||
|
R = scout.OriginR ?? home?.R ?? 0,
|
||||||
|
Status = "Idle",
|
||||||
|
MovementPerTick = movementPerTick,
|
||||||
|
MovementRemaining = movementPerTick,
|
||||||
|
LastMovementRefillAtUtc = now,
|
||||||
|
SourceTrainingId = scout.Id,
|
||||||
|
CreatedAtUtc = now,
|
||||||
|
UpdatedAtUtc = now
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _units.InsertOneAsync(unit);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _training.UpdateOneAsync(
|
||||||
|
x => x.Id == scout.Id,
|
||||||
|
Builders<UnitTraining>.Update
|
||||||
|
.Set(x => x.Status, "Consumed")
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string? NormalizeUnitType(string unitType)
|
private static string? NormalizeUnitType(string unitType)
|
||||||
{
|
{
|
||||||
var trimmed = unitType.Trim();
|
var trimmed = unitType.Trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user