Adding upkeep
This commit is contained in:
@@ -22,6 +22,7 @@ var build_request: HTTPRequest
|
|||||||
var refresh_request: HTTPRequest
|
var refresh_request: HTTPRequest
|
||||||
var resources_timer: Timer
|
var resources_timer: Timer
|
||||||
var auth_check_timer: Timer
|
var auth_check_timer: Timer
|
||||||
|
var refresh_prompt_timer: Timer
|
||||||
var refresh_dialog: ConfirmationDialog
|
var refresh_dialog: ConfirmationDialog
|
||||||
var session: Dictionary = {}
|
var session: Dictionary = {}
|
||||||
var escape_overlay: Control
|
var escape_overlay: Control
|
||||||
@@ -314,6 +315,12 @@ func _build_ui() -> void:
|
|||||||
auth_check_timer.timeout.connect(_check_auth_expiry)
|
auth_check_timer.timeout.connect(_check_auth_expiry)
|
||||||
add_child(auth_check_timer)
|
add_child(auth_check_timer)
|
||||||
|
|
||||||
|
refresh_prompt_timer = Timer.new()
|
||||||
|
refresh_prompt_timer.one_shot = true
|
||||||
|
refresh_prompt_timer.autostart = false
|
||||||
|
refresh_prompt_timer.timeout.connect(_on_refresh_prompt_timeout)
|
||||||
|
add_child(refresh_prompt_timer)
|
||||||
|
|
||||||
resources_timer = Timer.new()
|
resources_timer = Timer.new()
|
||||||
resources_timer.wait_time = RESOURCE_REFRESH_SECONDS
|
resources_timer.wait_time = RESOURCE_REFRESH_SECONDS
|
||||||
resources_timer.autostart = false
|
resources_timer.autostart = false
|
||||||
@@ -764,21 +771,29 @@ func _check_auth_expiry() -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if seconds_remaining <= TOKEN_REFRESH_PROMPT_SECONDS:
|
if seconds_remaining <= TOKEN_REFRESH_PROMPT_SECONDS:
|
||||||
_prompt_refresh_or_logout("Your session expires in %d seconds. Refresh it now?" % int(ceil(seconds_remaining)))
|
_prompt_refresh_or_logout(
|
||||||
|
"Your session expires in %d seconds. Refresh it now?" % int(ceil(seconds_remaining)),
|
||||||
|
seconds_remaining
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _prompt_refresh_or_logout(message: String) -> void:
|
func _prompt_refresh_or_logout(message: String, timeout_seconds: float = TOKEN_REFRESH_PROMPT_SECONDS) -> void:
|
||||||
if refresh_prompt_active or refresh_request_active:
|
if refresh_prompt_active or refresh_request_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
refresh_prompt_active = true
|
refresh_prompt_active = true
|
||||||
|
if refresh_prompt_timer:
|
||||||
|
refresh_prompt_timer.wait_time = max(1.0, timeout_seconds)
|
||||||
|
refresh_prompt_timer.start()
|
||||||
if refresh_dialog:
|
if refresh_dialog:
|
||||||
refresh_dialog.dialog_text = message
|
refresh_dialog.dialog_text = "%s\n\nIf you do not respond before the session expires, you will be logged out." % message
|
||||||
refresh_dialog.popup_centered()
|
refresh_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_auth_token() -> void:
|
func _refresh_auth_token() -> void:
|
||||||
refresh_prompt_active = false
|
refresh_prompt_active = false
|
||||||
|
if refresh_prompt_timer:
|
||||||
|
refresh_prompt_timer.stop()
|
||||||
var username := str(session.get("username", ""))
|
var username := str(session.get("username", ""))
|
||||||
var refresh_token := str(session.get("refreshToken", ""))
|
var refresh_token := str(session.get("refreshToken", ""))
|
||||||
if username.is_empty() or refresh_token.is_empty():
|
if username.is_empty() or refresh_token.is_empty():
|
||||||
@@ -820,6 +835,14 @@ func _on_refresh_request_completed(result: int, response_code: int, _headers: Pa
|
|||||||
_check_auth_expiry()
|
_check_auth_expiry()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_refresh_prompt_timeout() -> void:
|
||||||
|
if refresh_prompt_active and not refresh_request_active:
|
||||||
|
refresh_prompt_active = false
|
||||||
|
if refresh_dialog:
|
||||||
|
refresh_dialog.hide()
|
||||||
|
_logout()
|
||||||
|
|
||||||
|
|
||||||
func _save_session(next_session: Dictionary) -> void:
|
func _save_session(next_session: Dictionary) -> void:
|
||||||
var file := FileAccess.open(SESSION_PATH, FileAccess.WRITE)
|
var file := FileAccess.open(SESSION_PATH, FileAccess.WRITE)
|
||||||
if file:
|
if file:
|
||||||
@@ -949,7 +972,7 @@ func _load_resources() -> void:
|
|||||||
func _on_resources_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
func _on_resources_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
if response_code == 401:
|
if response_code == 401:
|
||||||
_prompt_refresh_or_logout("Your session expired. Refresh it now?")
|
_prompt_refresh_or_logout("Your session expired. Refresh it now?", 10.0)
|
||||||
return
|
return
|
||||||
coordinate_label.text = "Resource request failed. result=%d status=%d body=%s" % [result, response_code, body.get_string_from_utf8()]
|
coordinate_label.text = "Resource request failed. result=%d status=%d body=%s" % [result, response_code, body.get_string_from_utf8()]
|
||||||
return
|
return
|
||||||
@@ -1162,7 +1185,7 @@ func _move_selected_archers(to_q: int, to_r: int) -> void:
|
|||||||
if move_units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
if move_units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||||
return
|
return
|
||||||
|
|
||||||
coordinate_label.text = "Moving %d Archers to q:%d r:%d..." % [selected_archer_count, to_q, to_r]
|
coordinate_label.text = "Ordering %d Archers to move to q:%d r:%d..." % [selected_archer_count, to_q, to_r]
|
||||||
var error: int = move_units_request.request(
|
var error: int = move_units_request.request(
|
||||||
LOCATIONS_BASE_URL + "/api/units/move",
|
LOCATIONS_BASE_URL + "/api/units/move",
|
||||||
PackedStringArray([
|
PackedStringArray([
|
||||||
@@ -1194,8 +1217,9 @@ func _on_move_units_request_completed(result: int, response_code: int, _headers:
|
|||||||
return
|
return
|
||||||
|
|
||||||
_clear_selected_archers()
|
_clear_selected_archers()
|
||||||
coordinate_label.text = "Archer squad moved."
|
coordinate_label.text = "Archer squad movement ordered. It will arrive on the next tick."
|
||||||
_load_visible_locations()
|
_load_visible_locations()
|
||||||
|
_load_units()
|
||||||
|
|
||||||
|
|
||||||
func _request_worker_action(endpoint: String, body: Dictionary) -> void:
|
func _request_worker_action(endpoint: String, body: Dictionary) -> void:
|
||||||
@@ -1230,8 +1254,9 @@ func _on_worker_request_completed(result: int, response_code: int, _headers: Pac
|
|||||||
return
|
return
|
||||||
|
|
||||||
_clear_selected_archers()
|
_clear_selected_archers()
|
||||||
coordinate_label.text = "Worker assignment updated."
|
coordinate_label.text = "Worker assignment ordered. The peasant will travel one hex per tick."
|
||||||
_load_visible_locations()
|
_load_visible_locations()
|
||||||
|
_load_units()
|
||||||
|
|
||||||
|
|
||||||
func _claim_selected_tile() -> void:
|
func _claim_selected_tile() -> void:
|
||||||
@@ -1629,6 +1654,11 @@ func _toggle_escape_menu() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _logout() -> void:
|
func _logout() -> void:
|
||||||
|
refresh_prompt_active = false
|
||||||
|
if refresh_prompt_timer:
|
||||||
|
refresh_prompt_timer.stop()
|
||||||
|
if refresh_dialog:
|
||||||
|
refresh_dialog.hide()
|
||||||
if FileAccess.file_exists(SESSION_PATH):
|
if FileAccess.file_exists(SESSION_PATH):
|
||||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(SESSION_PATH))
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(SESSION_PATH))
|
||||||
get_tree().remove_meta("auth_session")
|
get_tree().remove_meta("auth_session")
|
||||||
|
|||||||
@@ -821,7 +821,8 @@ func _archer_count_at(q: int, r: int) -> int:
|
|||||||
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:
|
||||||
if str(unit.get("unitType", "")) != "Peasant" or str(unit.get("status", "")) != "Idle":
|
var status := str(unit.get("status", ""))
|
||||||
|
if str(unit.get("unitType", "")) != "Peasant" or (status != "Idle" 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
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ public class UnitInstanceResponse
|
|||||||
public int Q { get; set; }
|
public int Q { get; set; }
|
||||||
public int R { get; set; }
|
public int R { get; set; }
|
||||||
public string Status { get; set; } = "";
|
public string Status { get; set; } = "";
|
||||||
|
public int MovementPerTick { get; set; }
|
||||||
|
public int MovementRemaining { get; set; }
|
||||||
|
public DateTime LastMovementRefillAtUtc { get; set; }
|
||||||
|
public int? DestinationQ { get; set; }
|
||||||
|
public int? DestinationR { get; set; }
|
||||||
|
public DateTime? MovementStartedAtUtc { get; set; }
|
||||||
|
public DateTime? MovementCompletesAtUtc { get; set; }
|
||||||
public DateTime CreatedAtUtc { get; set; }
|
public DateTime CreatedAtUtc { get; set; }
|
||||||
public DateTime UpdatedAtUtc { get; set; }
|
public DateTime UpdatedAtUtc { get; set; }
|
||||||
|
|
||||||
@@ -67,6 +74,13 @@ public class UnitInstanceResponse
|
|||||||
Q = unit.Q,
|
Q = unit.Q,
|
||||||
R = unit.R,
|
R = unit.R,
|
||||||
Status = unit.Status,
|
Status = unit.Status,
|
||||||
|
MovementPerTick = unit.MovementPerTick,
|
||||||
|
MovementRemaining = unit.MovementRemaining,
|
||||||
|
LastMovementRefillAtUtc = unit.LastMovementRefillAtUtc,
|
||||||
|
DestinationQ = unit.DestinationQ,
|
||||||
|
DestinationR = unit.DestinationR,
|
||||||
|
MovementStartedAtUtc = unit.MovementStartedAtUtc,
|
||||||
|
MovementCompletesAtUtc = unit.MovementCompletesAtUtc,
|
||||||
CreatedAtUtc = unit.CreatedAtUtc,
|
CreatedAtUtc = unit.CreatedAtUtc,
|
||||||
UpdatedAtUtc = unit.UpdatedAtUtc
|
UpdatedAtUtc = unit.UpdatedAtUtc
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public class UnitInstance
|
|||||||
public int Q { get; set; }
|
public int Q { get; set; }
|
||||||
public int R { get; set; }
|
public int R { get; set; }
|
||||||
public string Status { get; set; } = "Idle";
|
public string Status { get; set; } = "Idle";
|
||||||
|
public int MovementPerTick { get; set; } = 1;
|
||||||
|
public int MovementRemaining { get; set; } = 1;
|
||||||
|
public DateTime LastMovementRefillAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
public int? DestinationQ { get; set; }
|
||||||
|
public int? DestinationR { get; set; }
|
||||||
|
public DateTime? MovementStartedAtUtc { get; set; }
|
||||||
|
public DateTime? MovementCompletesAtUtc { get; set; }
|
||||||
public string? SourceTrainingId { get; set; }
|
public string? SourceTrainingId { get; set; }
|
||||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class GameSettingsService
|
|||||||
public const string ArcherGoldCost = "ArcherGoldCost";
|
public const string ArcherGoldCost = "ArcherGoldCost";
|
||||||
public const string ArcherWoodCost = "ArcherWoodCost";
|
public const string ArcherWoodCost = "ArcherWoodCost";
|
||||||
public const string PeasantGoldCost = "PeasantGoldCost";
|
public const string PeasantGoldCost = "PeasantGoldCost";
|
||||||
|
public const string UnitFoodPerMinute = "UnitFoodPerMinute";
|
||||||
|
public const string UnitMovementPerTick = "UnitMovementPerTick";
|
||||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||||
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ public class LocationService
|
|||||||
public async Task<Location?> AssignWorkerAsync(string username, AssignWorkerRequest request)
|
public async Task<Location?> AssignWorkerAsync(string username, AssignWorkerRequest request)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
await CompletePendingWorkerAssignmentsAsync(username);
|
||||||
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) ||
|
||||||
@@ -308,36 +309,19 @@ public class LocationService
|
|||||||
var claimed = await _units.UpdateOneAsync(
|
var claimed = await _units.UpdateOneAsync(
|
||||||
x => x.Id == unit.Id && x.Status == "Idle",
|
x => x.Id == unit.Id && x.Status == "Idle",
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
.Set(x => x.Q, request.Q)
|
.Set(x => x.Status, "MovingToWork")
|
||||||
.Set(x => x.R, request.R)
|
.Set(x => x.MovementRemaining, 0)
|
||||||
.Set(x => x.Status, "Working")
|
.Set(x => x.DestinationQ, request.Q)
|
||||||
|
.Set(x => x.DestinationR, request.R)
|
||||||
|
.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.UpdatedAtUtc, now));
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
if (claimed.ModifiedCount == 0)
|
if (claimed.ModifiedCount == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var updated = await _locations.FindOneAndUpdateAsync(
|
return location;
|
||||||
x => x.Id == location.Id && (x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
|
||||||
Builders<Location>.Update
|
|
||||||
.Set(x => x.WorkerUnitId, unit.Id)
|
|
||||||
.Set(x => x.WorkerUnitType, "Peasant")
|
|
||||||
.Set(x => x.LastResourceGeneratedAtUtc, now)
|
|
||||||
.Set(x => x.UpdatedAtUtc, now),
|
|
||||||
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
|
|
||||||
|
|
||||||
if (updated is null)
|
|
||||||
{
|
|
||||||
await _units.UpdateOneAsync(
|
|
||||||
x => x.Id == unit.Id,
|
|
||||||
Builders<UnitInstance>.Update
|
|
||||||
.Set(x => x.Status, "Idle")
|
|
||||||
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await RecalculateWorkerGenerationAsync(username);
|
|
||||||
return updated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Location?> DeassignWorkerAsync(string username, DeassignWorkerRequest request)
|
public async Task<Location?> DeassignWorkerAsync(string username, DeassignWorkerRequest request)
|
||||||
@@ -433,6 +417,7 @@ public class LocationService
|
|||||||
public async Task<List<LocationResponse>> GetVisibleAsync(string username, int? visibilityDepth = null)
|
public async Task<List<LocationResponse>> GetVisibleAsync(string username, int? visibilityDepth = null)
|
||||||
{
|
{
|
||||||
await CompleteFinishedBuildingsAsync(username);
|
await CompleteFinishedBuildingsAsync(username);
|
||||||
|
await CompletePendingWorkerAssignmentsAsync(username);
|
||||||
await RecalculateWorkerGenerationAsync(username);
|
await RecalculateWorkerGenerationAsync(username);
|
||||||
|
|
||||||
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
var ownedLocations = await _locations.Find(x => x.OwnerUsername == username).ToListAsync();
|
||||||
@@ -500,6 +485,7 @@ public class LocationService
|
|||||||
|
|
||||||
public async Task<LocationResponse?> GetVisibleLocationAsync(string username, int q, int r)
|
public async Task<LocationResponse?> GetVisibleLocationAsync(string username, int q, int r)
|
||||||
{
|
{
|
||||||
|
await CompletePendingWorkerAssignmentsAsync(username);
|
||||||
if (!await CanSeeAsync(username, q, r))
|
if (!await CanSeeAsync(username, q, r))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -522,6 +508,98 @@ public class LocationService
|
|||||||
return generated.Count == 0 ? null : LocationResponse.FromLocation(generated[0]);
|
return generated.Count == 0 ? null : LocationResponse.FromLocation(generated[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CompletePendingWorkerAssignmentsAsync(string username)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var movingWorkers = await _units.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.UnitType == "Peasant" &&
|
||||||
|
x.Status == "MovingToWork" &&
|
||||||
|
x.MovementCompletesAtUtc != null &&
|
||||||
|
x.MovementCompletesAtUtc <= now)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var worker in movingWorkers)
|
||||||
|
{
|
||||||
|
if (worker.DestinationQ is null || worker.DestinationR is null)
|
||||||
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationQ = worker.DestinationQ.Value;
|
||||||
|
var destinationR = worker.DestinationR.Value;
|
||||||
|
var currentQ = worker.Q;
|
||||||
|
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);
|
||||||
|
nextTick = nextTick.AddMinutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||||
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, currentQ)
|
||||||
|
.Set(x => x.R, currentR)
|
||||||
|
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = await _locations.FindOneAndUpdateAsync(
|
||||||
|
x => x.OwnerUsername == username &&
|
||||||
|
x.Q == destinationQ &&
|
||||||
|
x.R == destinationR &&
|
||||||
|
!string.IsNullOrWhiteSpace(x.BuildingType) &&
|
||||||
|
x.BuildingStatus != "Building" &&
|
||||||
|
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||||
|
Builders<Location>.Update
|
||||||
|
.Set(x => x.WorkerUnitId, worker.Id)
|
||||||
|
.Set(x => x.WorkerUnitType, "Peasant")
|
||||||
|
.Set(x => x.LastResourceGeneratedAtUtc, now)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now),
|
||||||
|
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
|
||||||
|
|
||||||
|
var status = location is null ? "Idle" : "Working";
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, destinationQ)
|
||||||
|
.Set(x => x.R, destinationR)
|
||||||
|
.Set(x => x.Status, status)
|
||||||
|
.Set(x => x.LastMovementRefillAtUtc, now)
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movingWorkers.Count > 0)
|
||||||
|
{
|
||||||
|
await RecalculateWorkerGenerationAsync(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
||||||
|
{
|
||||||
|
return NeighborCoordinates(fromQ, fromR)
|
||||||
|
.OrderBy(x => HexDistance(x.Q, x.R, toQ, toR))
|
||||||
|
.ThenBy(x => x.Q)
|
||||||
|
.ThenBy(x => x.R)
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> CanSeeAsync(string username, int q, int r)
|
public async Task<bool> CanSeeAsync(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();
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ namespace LocationsApi.Services;
|
|||||||
|
|
||||||
public class ResourceService
|
public class ResourceService
|
||||||
{
|
{
|
||||||
|
private const string ScoutUnitType = "Scout";
|
||||||
|
private const string ArcherUnitType = "Archer";
|
||||||
|
private const string PeasantUnitType = "Peasant";
|
||||||
|
|
||||||
private readonly IMongoCollection<PlayerResources> _resources;
|
private readonly IMongoCollection<PlayerResources> _resources;
|
||||||
private readonly IMongoCollection<Location> _locations;
|
private readonly IMongoCollection<Location> _locations;
|
||||||
|
private readonly IMongoCollection<UnitTraining> _training;
|
||||||
|
private readonly IMongoCollection<UnitInstance> _units;
|
||||||
private readonly GameSettingsService _settings;
|
private readonly GameSettingsService _settings;
|
||||||
private readonly int _startingGold;
|
private readonly int _startingGold;
|
||||||
private readonly int _headquartersGoldPerMinute;
|
private readonly int _headquartersGoldPerMinute;
|
||||||
@@ -15,6 +21,7 @@ public class ResourceService
|
|||||||
private readonly int _townHallOreCapacity;
|
private readonly int _townHallOreCapacity;
|
||||||
private readonly int _townHallFoodCapacity;
|
private readonly int _townHallFoodCapacity;
|
||||||
private readonly int _productionBuildingResourceCapacity;
|
private readonly int _productionBuildingResourceCapacity;
|
||||||
|
private readonly int _unitFoodPerMinute;
|
||||||
|
|
||||||
public ResourceService(IConfiguration cfg, GameSettingsService settings)
|
public ResourceService(IConfiguration cfg, GameSettingsService settings)
|
||||||
{
|
{
|
||||||
@@ -24,6 +31,8 @@ public class ResourceService
|
|||||||
var database = client.GetDatabase(databaseName);
|
var database = client.GetDatabase(databaseName);
|
||||||
_resources = database.GetCollection<PlayerResources>("PlayerResources");
|
_resources = database.GetCollection<PlayerResources>("PlayerResources");
|
||||||
_locations = database.GetCollection<Location>("Locations");
|
_locations = database.GetCollection<Location>("Locations");
|
||||||
|
_training = database.GetCollection<UnitTraining>("UnitTraining");
|
||||||
|
_units = database.GetCollection<UnitInstance>("Units");
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_startingGold = int.TryParse(cfg["Resources:StartingGold"], out var startingGold) ? Math.Max(0, startingGold) : 100;
|
_startingGold = int.TryParse(cfg["Resources:StartingGold"], out var startingGold) ? Math.Max(0, startingGold) : 100;
|
||||||
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
||||||
@@ -44,6 +53,9 @@ public class ResourceService
|
|||||||
_productionBuildingResourceCapacity = int.TryParse(cfg["Buildings:ProductionBuildingResourceCapacity"], out var productionBuildingResourceCapacity)
|
_productionBuildingResourceCapacity = int.TryParse(cfg["Buildings:ProductionBuildingResourceCapacity"], out var productionBuildingResourceCapacity)
|
||||||
? Math.Max(0, productionBuildingResourceCapacity)
|
? Math.Max(0, productionBuildingResourceCapacity)
|
||||||
: 10;
|
: 10;
|
||||||
|
_unitFoodPerMinute = int.TryParse(cfg["Units:UnitFoodPerMinute"], out var unitFoodPerMinute)
|
||||||
|
? Math.Max(0, unitFoodPerMinute)
|
||||||
|
: 1;
|
||||||
|
|
||||||
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
||||||
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
||||||
@@ -92,7 +104,8 @@ public class ResourceService
|
|||||||
public async Task<PlayerResources> GetAsync(string username)
|
public async Task<PlayerResources> GetAsync(string username)
|
||||||
{
|
{
|
||||||
var resources = await EnsureAsync(username);
|
var resources = await EnsureAsync(username);
|
||||||
return await ApplyProductionBuildingDeliveryAsync(resources);
|
resources = await ApplyProductionBuildingDeliveryAsync(resources);
|
||||||
|
return await ApplyFoodUpkeepAsync(resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> TrySpendGoldAsync(string username, int amount)
|
public async Task<bool> TrySpendGoldAsync(string username, int amount)
|
||||||
@@ -300,6 +313,7 @@ public class ResourceService
|
|||||||
private async Task<PlayerResources> ApplyProductionBuildingDeliveryAsync(PlayerResources resources)
|
private async Task<PlayerResources> ApplyProductionBuildingDeliveryAsync(PlayerResources resources)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
await CompletePendingWorkerAssignmentsAsync(resources.Username, now);
|
||||||
var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity);
|
var woodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallWoodCapacity, _townHallWoodCapacity);
|
||||||
var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity);
|
var oreCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallOreCapacity, _townHallOreCapacity);
|
||||||
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
var foodCapacity = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodCapacity, _townHallFoodCapacity);
|
||||||
@@ -377,4 +391,239 @@ public class ResourceService
|
|||||||
ReturnDocument = ReturnDocument.After
|
ReturnDocument = ReturnDocument.After
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<PlayerResources> ApplyFoodUpkeepAsync(PlayerResources resources)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var unitFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.UnitFoodPerMinute, _unitFoodPerMinute);
|
||||||
|
var foodProductionPerMinute = await CalculateFoodProductionPerMinuteAsync(resources.Username);
|
||||||
|
var units = await GetFoodConsumingUnitsAsync(resources.Username);
|
||||||
|
var upkeepPerMinute = units.Count * unitFoodPerMinute;
|
||||||
|
var netFoodPerMinute = foodProductionPerMinute - upkeepPerMinute;
|
||||||
|
var elapsedMinutes = upkeepPerMinute <= 0 || resources.LastFoodGeneratedAtUtc.Year < 2000
|
||||||
|
? 0
|
||||||
|
: (int)Math.Floor((now - resources.LastFoodGeneratedAtUtc).TotalMinutes);
|
||||||
|
var food = resources.Food;
|
||||||
|
var removedUnits = new List<FoodConsumingUnit>();
|
||||||
|
|
||||||
|
for (var minute = 0; minute < elapsedMinutes && units.Count > 0; minute++)
|
||||||
|
{
|
||||||
|
var neededFood = units.Count * unitFoodPerMinute;
|
||||||
|
if (food >= neededFood)
|
||||||
|
{
|
||||||
|
food -= neededFood;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortage = neededFood - food;
|
||||||
|
food = 0;
|
||||||
|
var unitsToRemove = Math.Min(shortage, units.Count);
|
||||||
|
for (var i = 0; i < unitsToRemove; i++)
|
||||||
|
{
|
||||||
|
var index = Random.Shared.Next(units.Count);
|
||||||
|
removedUnits.Add(units[index]);
|
||||||
|
units.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
netFoodPerMinute = foodProductionPerMinute - (units.Count * unitFoodPerMinute);
|
||||||
|
if (elapsedMinutes <= 0 && removedUnits.Count == 0 && resources.Food == food && resources.FoodPerMinute == netFoodPerMinute)
|
||||||
|
{
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RemoveStarvedUnitsAsync(resources.Username, removedUnits, now);
|
||||||
|
|
||||||
|
var update = Builders<PlayerResources>.Update
|
||||||
|
.Set(x => x.FoodPerMinute, netFoodPerMinute)
|
||||||
|
.Set(x => x.Food, food)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
|
if (elapsedMinutes > 0)
|
||||||
|
{
|
||||||
|
update = update.Set(x => x.LastFoodGeneratedAtUtc, resources.LastFoodGeneratedAtUtc.AddMinutes(elapsedMinutes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _resources.FindOneAndUpdateAsync(
|
||||||
|
x => x.Username == resources.Username,
|
||||||
|
update,
|
||||||
|
new FindOneAndUpdateOptions<PlayerResources>
|
||||||
|
{
|
||||||
|
ReturnDocument = ReturnDocument.After
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<FoodConsumingUnit>> GetFoodConsumingUnitsAsync(string username)
|
||||||
|
{
|
||||||
|
var completedScouts = await _training.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.UnitType == ScoutUnitType &&
|
||||||
|
x.Status == "Completed")
|
||||||
|
.ToListAsync();
|
||||||
|
var persistedUnits = await _units.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
(x.UnitType == ArcherUnitType || x.UnitType == PeasantUnitType))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var units = completedScouts
|
||||||
|
.Select(x => new FoodConsumingUnit(x.Id, x.UnitType, true))
|
||||||
|
.ToList();
|
||||||
|
units.AddRange(persistedUnits.Select(x => new FoodConsumingUnit(x.Id, x.UnitType, false)));
|
||||||
|
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> CalculateFoodProductionPerMinuteAsync(string username)
|
||||||
|
{
|
||||||
|
var farmFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.FarmFoodPerMinute, 5);
|
||||||
|
var farms = await _locations.CountDocumentsAsync(x =>
|
||||||
|
x.OwnerUsername == username &&
|
||||||
|
!string.IsNullOrWhiteSpace(x.WorkerUnitId) &&
|
||||||
|
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") &&
|
||||||
|
x.BuildingType == "Farm");
|
||||||
|
|
||||||
|
return checked((int)farms) * farmFoodPerMinute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompletePendingWorkerAssignmentsAsync(string username, DateTime now)
|
||||||
|
{
|
||||||
|
var movingWorkers = await _units.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.UnitType == "Peasant" &&
|
||||||
|
x.Status == "MovingToWork" &&
|
||||||
|
x.MovementCompletesAtUtc != null &&
|
||||||
|
x.MovementCompletesAtUtc <= now)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var worker in movingWorkers)
|
||||||
|
{
|
||||||
|
if (worker.DestinationQ is null || worker.DestinationR is null)
|
||||||
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationQ = worker.DestinationQ.Value;
|
||||||
|
var destinationR = worker.DestinationR.Value;
|
||||||
|
var currentQ = worker.Q;
|
||||||
|
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);
|
||||||
|
nextTick = nextTick.AddMinutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HexDistance(currentQ, currentR, destinationQ, destinationR) > 1)
|
||||||
|
{
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, currentQ)
|
||||||
|
.Set(x => x.R, currentR)
|
||||||
|
.Set(x => x.MovementCompletesAtUtc, nextTick)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = await _locations.FindOneAndUpdateAsync(
|
||||||
|
x => x.OwnerUsername == username &&
|
||||||
|
x.Q == destinationQ &&
|
||||||
|
x.R == destinationR &&
|
||||||
|
!string.IsNullOrWhiteSpace(x.BuildingType) &&
|
||||||
|
x.BuildingStatus != "Building" &&
|
||||||
|
(x.WorkerUnitId == null || x.WorkerUnitId == ""),
|
||||||
|
Builders<Location>.Update
|
||||||
|
.Set(x => x.WorkerUnitId, worker.Id)
|
||||||
|
.Set(x => x.WorkerUnitType, "Peasant")
|
||||||
|
.Set(x => x.LastResourceGeneratedAtUtc, now)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now),
|
||||||
|
new FindOneAndUpdateOptions<Location> { ReturnDocument = ReturnDocument.After });
|
||||||
|
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == worker.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, destinationQ)
|
||||||
|
.Set(x => x.R, destinationR)
|
||||||
|
.Set(x => x.Status, location is null ? "Idle" : "Working")
|
||||||
|
.Set(x => x.LastMovementRefillAtUtc, now)
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int q, int r) NextStepToward(int fromQ, int fromR, int toQ, int toR)
|
||||||
|
{
|
||||||
|
return NeighborCoordinates(fromQ, fromR)
|
||||||
|
.OrderBy(x => HexDistance(x.q, x.r, toQ, toR))
|
||||||
|
.ThenBy(x => x.q)
|
||||||
|
.ThenBy(x => x.r)
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(int q, int r)> NeighborCoordinates(int q, int r)
|
||||||
|
{
|
||||||
|
yield return (q + 1, r);
|
||||||
|
yield return (q + 1, r - 1);
|
||||||
|
yield return (q, r - 1);
|
||||||
|
yield return (q - 1, r);
|
||||||
|
yield return (q - 1, r + 1);
|
||||||
|
yield return (q, r + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int HexDistance(int q1, int r1, int q2, int r2)
|
||||||
|
{
|
||||||
|
var s1 = -q1 - r1;
|
||||||
|
var s2 = -q2 - r2;
|
||||||
|
return Math.Max(Math.Max(Math.Abs(q1 - q2), Math.Abs(r1 - r2)), Math.Abs(s1 - s2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveStarvedUnitsAsync(string username, List<FoodConsumingUnit> units, DateTime now)
|
||||||
|
{
|
||||||
|
if (units.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoutIds = units
|
||||||
|
.Where(x => x.IsTrainingRecord)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
var instanceIds = units
|
||||||
|
.Where(x => !x.IsTrainingRecord)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (scoutIds.Count > 0)
|
||||||
|
{
|
||||||
|
await _training.DeleteManyAsync(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.UnitType == ScoutUnitType &&
|
||||||
|
scoutIds.Contains(x.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instanceIds.Count > 0)
|
||||||
|
{
|
||||||
|
await _locations.UpdateManyAsync(
|
||||||
|
x => x.OwnerUsername == username && x.WorkerUnitId != null && instanceIds.Contains(x.WorkerUnitId),
|
||||||
|
Builders<Location>.Update
|
||||||
|
.Unset(x => x.WorkerUnitId)
|
||||||
|
.Unset(x => x.WorkerUnitType)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
await _units.DeleteManyAsync(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
instanceIds.Contains(x.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record FoodConsumingUnit(string Id, string UnitType, bool IsTrainingRecord);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class UnitTrainingService
|
|||||||
private readonly int _defaultScoutTrainingSeconds;
|
private readonly int _defaultScoutTrainingSeconds;
|
||||||
private readonly int _defaultArcherTrainingSeconds;
|
private readonly int _defaultArcherTrainingSeconds;
|
||||||
private readonly int _defaultPeasantTrainingSeconds;
|
private readonly int _defaultPeasantTrainingSeconds;
|
||||||
|
private readonly int _unitMovementPerTick;
|
||||||
private readonly int _visibilityDepth;
|
private readonly int _visibilityDepth;
|
||||||
|
|
||||||
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||||
@@ -47,6 +48,9 @@ public class UnitTrainingService
|
|||||||
_defaultPeasantTrainingSeconds = int.TryParse(cfg["Units:PeasantTrainingSeconds"], out var peasantTrainingSeconds)
|
_defaultPeasantTrainingSeconds = int.TryParse(cfg["Units:PeasantTrainingSeconds"], out var peasantTrainingSeconds)
|
||||||
? Math.Max(1, peasantTrainingSeconds)
|
? Math.Max(1, peasantTrainingSeconds)
|
||||||
: 60;
|
: 60;
|
||||||
|
_unitMovementPerTick = int.TryParse(cfg["Units:UnitMovementPerTick"], out var unitMovementPerTick)
|
||||||
|
? Math.Max(1, unitMovementPerTick)
|
||||||
|
: 1;
|
||||||
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
|
_visibilityDepth = int.TryParse(cfg["World:VisibilityDepth"], out var visibilityDepth) ? Math.Max(0, visibilityDepth) : 3;
|
||||||
|
|
||||||
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
||||||
@@ -65,6 +69,8 @@ public class UnitTrainingService
|
|||||||
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
|
||||||
{
|
{
|
||||||
await CompleteFinishedTrainingAsync(username);
|
await CompleteFinishedTrainingAsync(username);
|
||||||
|
await CompletePendingMovementAsync(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();
|
||||||
@@ -167,6 +173,9 @@ public class UnitTrainingService
|
|||||||
|
|
||||||
public async Task<List<UnitInstance>> MoveSquadAsync(string username, MoveUnitSquadRequest request)
|
public async Task<List<UnitInstance>> MoveSquadAsync(string username, MoveUnitSquadRequest request)
|
||||||
{
|
{
|
||||||
|
await CompletePendingMovementAsync(username);
|
||||||
|
await RefillMovementAsync(username);
|
||||||
|
|
||||||
var normalizedUnitType = NormalizeUnitType(request.UnitType);
|
var normalizedUnitType = NormalizeUnitType(request.UnitType);
|
||||||
if (normalizedUnitType != ArcherUnitType)
|
if (normalizedUnitType != ArcherUnitType)
|
||||||
{
|
{
|
||||||
@@ -192,6 +201,7 @@ public class UnitTrainingService
|
|||||||
var units = await _units.Find(x =>
|
var units = await _units.Find(x =>
|
||||||
x.Username == username &&
|
x.Username == username &&
|
||||||
x.UnitType == ArcherUnitType &&
|
x.UnitType == ArcherUnitType &&
|
||||||
|
x.Status == "Idle" &&
|
||||||
x.Q == request.FromQ &&
|
x.Q == request.FromQ &&
|
||||||
x.R == request.FromR)
|
x.R == request.FromR)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -201,24 +211,35 @@ public class UnitTrainingService
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (units.Any(x => x.MovementRemaining <= 0))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("That squad has no movement remaining this tick.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var movementCompletesAt = now.AddMinutes(1);
|
||||||
await _units.UpdateManyAsync(
|
await _units.UpdateManyAsync(
|
||||||
x => x.Username == username &&
|
x => x.Username == username &&
|
||||||
x.UnitType == ArcherUnitType &&
|
x.UnitType == ArcherUnitType &&
|
||||||
|
x.Status == "Idle" &&
|
||||||
x.Q == request.FromQ &&
|
x.Q == request.FromQ &&
|
||||||
x.R == request.FromR,
|
x.R == request.FromR,
|
||||||
Builders<UnitInstance>.Update
|
Builders<UnitInstance>.Update
|
||||||
.Set(x => x.Q, request.ToQ)
|
.Set(x => x.Status, "Moving")
|
||||||
.Set(x => x.R, request.ToR)
|
.Inc(x => x.MovementRemaining, -1)
|
||||||
.Set(x => x.Status, "Idle")
|
.Set(x => x.DestinationQ, request.ToQ)
|
||||||
|
.Set(x => x.DestinationR, request.ToR)
|
||||||
|
.Set(x => x.MovementStartedAtUtc, now)
|
||||||
|
.Set(x => x.MovementCompletesAtUtc, movementCompletesAt)
|
||||||
.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 &&
|
||||||
x.Q == request.ToQ &&
|
x.Status == "Moving" &&
|
||||||
x.R == request.ToR)
|
x.Q == request.FromQ &&
|
||||||
|
x.R == request.FromR &&
|
||||||
|
x.DestinationQ == request.ToQ &&
|
||||||
|
x.DestinationR == request.ToR)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +255,86 @@ public class UnitTrainingService
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CompletePendingMovementAsync(string username)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||||
|
var movingUnits = await _units.Find(x =>
|
||||||
|
x.Username == username &&
|
||||||
|
x.Status == "Moving" &&
|
||||||
|
x.MovementCompletesAtUtc != null &&
|
||||||
|
x.MovementCompletesAtUtc <= now)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var unit in movingUnits)
|
||||||
|
{
|
||||||
|
if (unit.DestinationQ is null || unit.DestinationR 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.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _units.UpdateOneAsync(
|
||||||
|
x => x.Id == unit.Id,
|
||||||
|
Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.Q, unit.DestinationQ.Value)
|
||||||
|
.Set(x => x.R, unit.DestinationR.Value)
|
||||||
|
.Set(x => x.Status, "Idle")
|
||||||
|
.Set(x => x.MovementPerTick, movementPerTick)
|
||||||
|
.Set(x => x.MovementRemaining, movementPerTick)
|
||||||
|
.Set(x => x.LastMovementRefillAtUtc, now)
|
||||||
|
.Unset(x => x.DestinationQ)
|
||||||
|
.Unset(x => x.DestinationR)
|
||||||
|
.Unset(x => x.MovementStartedAtUtc)
|
||||||
|
.Unset(x => x.MovementCompletesAtUtc)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now));
|
||||||
|
|
||||||
|
if (unit.UnitType == ArcherUnitType)
|
||||||
|
{
|
||||||
|
await ClaimTargetIfEmptyAsync(username, unit.DestinationQ.Value, unit.DestinationR.Value, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefillMovementAsync(string username)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||||
|
var units = await _units.Find(x => x.Username == username && x.Status != "Moving" && x.Status != "MovingToWork").ToListAsync();
|
||||||
|
|
||||||
|
foreach (var unit in units)
|
||||||
|
{
|
||||||
|
var lastRefill = unit.LastMovementRefillAtUtc.Year < 2000 ? now : unit.LastMovementRefillAtUtc;
|
||||||
|
var elapsedTicks = (int)Math.Floor((now - lastRefill).TotalMinutes);
|
||||||
|
var movementRemaining = elapsedTicks > 0
|
||||||
|
? movementPerTick
|
||||||
|
: Math.Min(unit.MovementRemaining, movementPerTick);
|
||||||
|
if (elapsedTicks <= 0 && unit.MovementPerTick == movementPerTick && unit.MovementRemaining == movementRemaining)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var update = Builders<UnitInstance>.Update
|
||||||
|
.Set(x => x.MovementPerTick, movementPerTick)
|
||||||
|
.Set(x => x.MovementRemaining, movementRemaining)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
if (elapsedTicks > 0)
|
||||||
|
{
|
||||||
|
update = update.Set(x => x.LastMovementRefillAtUtc, lastRefill.AddMinutes(elapsedTicks));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _units.UpdateOneAsync(x => x.Id == unit.Id, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CompleteFinishedBuildingsAsync(string username)
|
private async Task CompleteFinishedBuildingsAsync(string username)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -376,6 +477,7 @@ public class UnitTrainingService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var movementPerTick = await _settings.GetIntAsync(GameSettingsService.UnitMovementPerTick, _unitMovementPerTick);
|
||||||
var unit = new UnitInstance
|
var unit = new UnitInstance
|
||||||
{
|
{
|
||||||
Username = completed.Username,
|
Username = completed.Username,
|
||||||
@@ -383,6 +485,9 @@ public class UnitTrainingService
|
|||||||
Q = completed.OriginQ ?? 0,
|
Q = completed.OriginQ ?? 0,
|
||||||
R = completed.OriginR ?? 0,
|
R = completed.OriginR ?? 0,
|
||||||
Status = "Idle",
|
Status = "Idle",
|
||||||
|
MovementPerTick = movementPerTick,
|
||||||
|
MovementRemaining = movementPerTick,
|
||||||
|
LastMovementRefillAtUtc = now,
|
||||||
SourceTrainingId = completed.Id,
|
SourceTrainingId = completed.Id,
|
||||||
CreatedAtUtc = now,
|
CreatedAtUtc = now,
|
||||||
UpdatedAtUtc = now
|
UpdatedAtUtc = now
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
"ScoutTrainingSeconds": 60,
|
"ScoutTrainingSeconds": 60,
|
||||||
"PeasantGoldCost": 25,
|
"PeasantGoldCost": 25,
|
||||||
"PeasantTrainingSeconds": 60,
|
"PeasantTrainingSeconds": 60,
|
||||||
|
"UnitFoodPerMinute": 1,
|
||||||
|
"UnitMovementPerTick": 1,
|
||||||
"ArcherGoldCost": 100,
|
"ArcherGoldCost": 100,
|
||||||
"ArcherWoodCost": 100,
|
"ArcherWoodCost": 100,
|
||||||
"ArcherTrainingSeconds": 60
|
"ArcherTrainingSeconds": 60
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ data:
|
|||||||
Units__ScoutTrainingSeconds: "60"
|
Units__ScoutTrainingSeconds: "60"
|
||||||
Units__PeasantGoldCost: "25"
|
Units__PeasantGoldCost: "25"
|
||||||
Units__PeasantTrainingSeconds: "60"
|
Units__PeasantTrainingSeconds: "60"
|
||||||
|
Units__UnitFoodPerMinute: "1"
|
||||||
|
Units__UnitMovementPerTick: "1"
|
||||||
Units__ArcherGoldCost: "100"
|
Units__ArcherGoldCost: "100"
|
||||||
Units__ArcherWoodCost: "100"
|
Units__ArcherWoodCost: "100"
|
||||||
Units__ArcherTrainingSeconds: "60"
|
Units__ArcherTrainingSeconds: "60"
|
||||||
|
|||||||
Reference in New Issue
Block a user