From c0e60d622cf9f80286b7e19fc720fa85613d93c4 Mon Sep 17 00:00:00 2001 From: hz Date: Fri, 22 May 2026 03:11:32 -0500 Subject: [PATCH] adding delivery --- .../scenes/game/hex_grid_view.gd | 4 +- microservices/LocationsApi/Models/Location.cs | 1 + microservices/LocationsApi/Models/UnitDto.cs | 8 + .../LocationsApi/Models/UnitInstance.cs | 4 + .../LocationsApi/Services/LocationService.cs | 28 ++- .../LocationsApi/Services/ResourceService.cs | 179 ++++++++++++++++-- 6 files changed, 199 insertions(+), 25 deletions(-) diff --git a/game/territory-autobattler/scenes/game/hex_grid_view.gd b/game/territory-autobattler/scenes/game/hex_grid_view.gd index 27dacfe..c93375c 100644 --- a/game/territory-autobattler/scenes/game/hex_grid_view.gd +++ b/game/territory-autobattler/scenes/game/hex_grid_view.gd @@ -255,7 +255,7 @@ func _draw_control_layer(control_tiles: Array, control: String, layer_scale: flo func _draw_movement_indicators() -> void: for unit in unit_instances: var status := str(unit.get("status", "")) - if status != "Moving" and status != "MovingToWork": + if status != "Moving" and status != "MovingToWork" and status != "DeliveringToTownHall" and status != "ReturningToWork": continue if unit.get("destinationQ", null) == null or unit.get("destinationR", null) == null: continue @@ -1022,7 +1022,7 @@ func _peasant_count_at(q: int, r: int) -> int: var count := 0 for unit in unit_instances: var status := str(unit.get("status", "")) - if str(unit.get("unitType", "")) != "Peasant" or (status != "Idle" and status != "Moving" and status != "MovingToWork"): + if str(unit.get("unitType", "")) != "Peasant" or (status != "Idle" and status != "Moving" and status != "MovingToWork" and status != "DeliveringToTownHall" and status != "ReturningToWork"): continue if int(unit.get("q", 0)) == q and int(unit.get("r", 0)) == r: count += 1 diff --git a/microservices/LocationsApi/Models/Location.cs b/microservices/LocationsApi/Models/Location.cs index ca74df9..0051037 100644 --- a/microservices/LocationsApi/Models/Location.cs +++ b/microservices/LocationsApi/Models/Location.cs @@ -26,6 +26,7 @@ public class Location public int StoredResourceAmount { get; set; } public int StoredResourceCapacity { get; set; } = 10; public DateTime? LastResourceGeneratedAtUtc { get; set; } + public DateTime? DeliveryCycleStartedAtUtc { get; set; } public DateTime? HomeAssignedAtUtc { get; set; } public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow; diff --git a/microservices/LocationsApi/Models/UnitDto.cs b/microservices/LocationsApi/Models/UnitDto.cs index 7f1002a..d505184 100644 --- a/microservices/LocationsApi/Models/UnitDto.cs +++ b/microservices/LocationsApi/Models/UnitDto.cs @@ -77,6 +77,10 @@ public class UnitInstanceResponse public int? DestinationQ { get; set; } public int? DestinationR { get; set; } public bool ClaimOnArrival { get; set; } + public int? WorksiteQ { get; set; } + public int? WorksiteR { get; set; } + public string? CarryingResourceType { get; set; } + public int CarryingResourceAmount { get; set; } public DateTime? MovementStartedAtUtc { get; set; } public DateTime? MovementCompletesAtUtc { get; set; } public DateTime CreatedAtUtc { get; set; } @@ -96,6 +100,10 @@ public class UnitInstanceResponse DestinationQ = unit.DestinationQ, DestinationR = unit.DestinationR, ClaimOnArrival = unit.ClaimOnArrival, + WorksiteQ = unit.WorksiteQ, + WorksiteR = unit.WorksiteR, + CarryingResourceType = unit.CarryingResourceType, + CarryingResourceAmount = unit.CarryingResourceAmount, MovementStartedAtUtc = unit.MovementStartedAtUtc, MovementCompletesAtUtc = unit.MovementCompletesAtUtc, CreatedAtUtc = unit.CreatedAtUtc, diff --git a/microservices/LocationsApi/Models/UnitInstance.cs b/microservices/LocationsApi/Models/UnitInstance.cs index a407aa5..5a562d3 100644 --- a/microservices/LocationsApi/Models/UnitInstance.cs +++ b/microservices/LocationsApi/Models/UnitInstance.cs @@ -20,6 +20,10 @@ public class UnitInstance public int? DestinationQ { get; set; } public int? DestinationR { get; set; } public bool ClaimOnArrival { get; set; } + public int? WorksiteQ { get; set; } + public int? WorksiteR { get; set; } + public string? CarryingResourceType { get; set; } + public int CarryingResourceAmount { get; set; } public DateTime? MovementStartedAtUtc { get; set; } public DateTime? MovementCompletesAtUtc { get; set; } public string? SourceTrainingId { get; set; } diff --git a/microservices/LocationsApi/Services/LocationService.cs b/microservices/LocationsApi/Services/LocationService.cs index 5da0ef0..b5928ad 100644 --- a/microservices/LocationsApi/Services/LocationService.cs +++ b/microservices/LocationsApi/Services/LocationService.cs @@ -337,6 +337,8 @@ public class LocationService .Set(x => x.MovementRemaining, 0) .Set(x => x.DestinationQ, request.Q) .Set(x => x.DestinationR, request.R) + .Set(x => x.WorksiteQ, request.Q) + .Set(x => x.WorksiteR, request.R) .Set(x => x.MovementStartedAtUtc, now) .Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1)) .Set(x => x.UpdatedAtUtc, now)); @@ -360,11 +362,19 @@ public class LocationService } await _units.UpdateOneAsync( - x => x.Id == location.WorkerUnitId && x.Username == username && x.Status == "Working", + x => x.Id == location.WorkerUnitId && x.Username == username, Builders.Update .Set(x => x.Q, request.Q) .Set(x => x.R, request.R) .Set(x => x.Status, "Idle") + .Unset(x => x.WorksiteQ) + .Unset(x => x.WorksiteR) + .Unset(x => x.DestinationQ) + .Unset(x => x.DestinationR) + .Unset(x => x.CarryingResourceType) + .Set(x => x.CarryingResourceAmount, 0) + .Unset(x => x.MovementStartedAtUtc) + .Unset(x => x.MovementCompletesAtUtc) .Set(x => x.UpdatedAtUtc, now)); var updated = await _locations.FindOneAndUpdateAsync( @@ -372,6 +382,7 @@ public class LocationService Builders.Update .Unset(x => x.WorkerUnitId) .Unset(x => x.WorkerUnitType) + .Unset(x => x.DeliveryCycleStartedAtUtc) .Set(x => x.UpdatedAtUtc, now), new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); @@ -404,6 +415,14 @@ public class LocationService .Set(x => x.R, r) .Set(x => x.Status, "Idle") .Set(x => x.MovementRemaining, 1) + .Unset(x => x.WorksiteQ) + .Unset(x => x.WorksiteR) + .Unset(x => x.DestinationQ) + .Unset(x => x.DestinationR) + .Unset(x => x.CarryingResourceType) + .Set(x => x.CarryingResourceAmount, 0) + .Unset(x => x.MovementStartedAtUtc) + .Unset(x => x.MovementCompletesAtUtc) .Set(x => x.UpdatedAtUtc, now)); } @@ -420,6 +439,7 @@ public class LocationService .Set(x => x.StoredResourceAmount, 0) .Set(x => x.StoredResourceCapacity, 10) .Unset(x => x.LastResourceGeneratedAtUtc) + .Unset(x => x.DeliveryCycleStartedAtUtc) .Set(x => x.UpdatedAtUtc, now), new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); @@ -649,6 +669,7 @@ public class LocationService var currentQ = worker.Q; var currentR = worker.R; var elapsedSteps = Math.Max(1, (int)Math.Floor((now - worker.MovementCompletesAtUtc!.Value).TotalMinutes) + 1); + var usedSteps = 0; var movementBlocked = false; while (elapsedSteps > 0 && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1) @@ -672,6 +693,7 @@ public class LocationService currentQ = nextStep.Value.q; currentR = nextStep.Value.r; elapsedSteps--; + usedSteps++; } if (movementBlocked) @@ -689,6 +711,7 @@ public class LocationService continue; } + var assignedAt = worker.MovementCompletesAtUtc.Value.AddMinutes(Math.Max(0, usedSteps - 1)); var location = await _locations.FindOneAndUpdateAsync( x => x.OwnerUsername == username && x.Q == destinationQ && @@ -699,7 +722,8 @@ public class LocationService Builders.Update .Set(x => x.WorkerUnitId, worker.Id) .Set(x => x.WorkerUnitType, "Peasant") - .Set(x => x.LastResourceGeneratedAtUtc, now) + .Set(x => x.LastResourceGeneratedAtUtc, assignedAt) + .Set(x => x.DeliveryCycleStartedAtUtc, assignedAt) .Set(x => x.UpdatedAtUtc, now), new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); diff --git a/microservices/LocationsApi/Services/ResourceService.cs b/microservices/LocationsApi/Services/ResourceService.cs index 5f1d6a8..ee4f400 100644 --- a/microservices/LocationsApi/Services/ResourceService.cs +++ b/microservices/LocationsApi/Services/ResourceService.cs @@ -108,8 +108,8 @@ public class ResourceService public async Task GetAsync(string username) { var resources = await EnsureAsync(username); - resources = await ApplyFoodTickAsync(resources); - return await ApplyProductionBuildingDeliveryAsync(resources, includeFood: false); + resources = await ApplyProductionBuildingDeliveryAsync(resources); + return await ApplyFoodTickAsync(resources); } public async Task TrySpendGoldAsync(string username, int amount) @@ -340,6 +340,7 @@ public class ResourceService (x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == "") && (x.BuildingType == "LogCamp" || x.BuildingType == "Mine" || (includeFood && x.BuildingType == "Farm"))) .ToListAsync(); + var home = await _locations.Find(x => x.OwnerUsername == resources.Username && x.Kind == "Home").FirstOrDefaultAsync(); var wood = Math.Min(resources.Wood, woodCapacity); var ore = Math.Min(resources.Ore, oreCapacity); @@ -347,6 +348,11 @@ public class ResourceService foreach (var building in buildings) { + if (home is null) + { + break; + } + var rate = building.BuildingType switch { "LogCamp" => logCampWoodPerMinute, @@ -355,29 +361,47 @@ public class ResourceService _ => 0 }; var lastGenerated = building.LastResourceGeneratedAtUtc ?? building.BuildingBuiltAtUtc ?? now; + var route = DirectRoute((building.Q, building.R), (home.Q, home.R)); + var oneWayMinutes = Math.Max(1, route.Count - 1); + var cycleMinutes = Math.Max(1, oneWayMinutes * 2); + var cycleStarted = building.DeliveryCycleStartedAtUtc ?? lastGenerated; var elapsedMinutes = rate <= 0 ? 0 : (int)Math.Floor((now - lastGenerated).TotalMinutes); - var stored = Math.Min(buildingCapacity, Math.Max(0, building.StoredResourceAmount) + (elapsedMinutes * rate)); + var stored = Math.Min(buildingCapacity, Math.Max(0, building.StoredResourceAmount)); - var delivered = 0; - if (building.BuildingType == "LogCamp") + for (var minute = 1; minute <= elapsedMinutes; minute++) { - delivered = Math.Min(stored, Math.Max(0, woodCapacity - wood)); - wood += delivered; - } - else if (building.BuildingType == "Mine") - { - delivered = Math.Min(stored, Math.Max(0, oreCapacity - ore)); - ore += delivered; - } - else if (building.BuildingType == "Farm") - { - delivered = Math.Min(stored, Math.Max(0, foodCapacity - food)); - food += delivered; + var minuteAt = lastGenerated.AddMinutes(minute); + stored = Math.Min(buildingCapacity, stored + rate); + var cycleMinute = (int)Math.Floor((minuteAt - cycleStarted).TotalMinutes); + if (cycleMinute < oneWayMinutes || (cycleMinute - oneWayMinutes) % cycleMinutes != 0) + { + continue; + } + + var delivered = 0; + if (building.BuildingType == "LogCamp") + { + delivered = Math.Min(stored, Math.Max(0, woodCapacity - wood)); + wood += delivered; + } + else if (building.BuildingType == "Mine") + { + delivered = Math.Min(stored, Math.Max(0, oreCapacity - ore)); + ore += delivered; + } + else if (building.BuildingType == "Farm") + { + delivered = Math.Min(stored, Math.Max(0, foodCapacity - food)); + food += delivered; + } + + stored -= delivered; } var update = Builders.Update - .Set(x => x.StoredResourceAmount, stored - delivered) + .Set(x => x.StoredResourceAmount, stored) .Set(x => x.StoredResourceCapacity, buildingCapacity) + .Set(x => x.DeliveryCycleStartedAtUtc, cycleStarted) .Set(x => x.UpdatedAtUtc, now); if (elapsedMinutes > 0 || building.LastResourceGeneratedAtUtc is null) @@ -386,6 +410,7 @@ public class ResourceService } await _locations.UpdateOneAsync(x => x.Id == building.Id, update); + await UpdateDeliveryWorkerStateAsync(resources.Username, building, home, route, cycleStarted, oneWayMinutes, cycleMinutes, stored, buildingCapacity, now); } return await _resources.FindOneAndUpdateAsync( @@ -408,6 +433,7 @@ public class ResourceService { var now = DateTime.UtcNow; var unitFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.UnitFoodPerMinute, _unitFoodPerMinute); + var directFoodProductionPerMinute = await CalculateDirectFoodProductionPerMinuteAsync(resources.Username); var foodProductionPerMinute = await CalculateFoodProductionPerMinuteAsync(resources.Username); var units = await GetFoodConsumingUnitsAsync(resources.Username); var upkeepPerMinute = units.Count * unitFoodPerMinute; @@ -421,9 +447,9 @@ public class ResourceService for (var minute = 0; minute < elapsedMinutes; minute++) { - if (foodProductionPerMinute > 0) + if (directFoodProductionPerMinute > 0) { - food = Math.Min(foodCapacity, food + foodProductionPerMinute); + food = Math.Min(foodCapacity, food + directFoodProductionPerMinute); } var neededFood = units.Count * unitFoodPerMinute; @@ -497,6 +523,82 @@ public class ResourceService return units; } + private async Task UpdateDeliveryWorkerStateAsync( + string username, + Location building, + Location home, + List<(int q, int r)> route, + DateTime cycleStarted, + int oneWayMinutes, + int cycleMinutes, + int remainingStored, + int carryCapacity, + DateTime now) + { + if (string.IsNullOrWhiteSpace(building.WorkerUnitId) || route.Count == 0) + { + return; + } + + var resourceType = building.BuildingType switch + { + "LogCamp" => "Wood", + "Mine" => "Ore", + "Farm" => "Food", + _ => "" + }; + if (resourceType == "") + { + return; + } + + var elapsedCycleMinutes = Math.Max(0, (int)Math.Floor((now - cycleStarted).TotalMinutes)); + var phase = cycleMinutes <= 0 ? 0 : elapsedCycleMinutes % cycleMinutes; + var isDelivering = remainingStored > 0 && phase < oneWayMinutes; + var isReturning = phase >= oneWayMinutes; + if (!isDelivering && !isReturning) + { + await _units.UpdateOneAsync( + x => x.Id == building.WorkerUnitId && x.Username == username, + Builders.Update + .Set(x => x.Q, building.Q) + .Set(x => x.R, building.R) + .Set(x => x.Status, "Working") + .Set(x => x.WorksiteQ, building.Q) + .Set(x => x.WorksiteR, building.R) + .Unset(x => x.DestinationQ) + .Unset(x => x.DestinationR) + .Unset(x => x.CarryingResourceType) + .Set(x => x.CarryingResourceAmount, 0) + .Unset(x => x.MovementStartedAtUtc) + .Unset(x => x.MovementCompletesAtUtc) + .Set(x => x.UpdatedAtUtc, now)); + return; + } + + var routeIndex = isDelivering + ? Math.Min(route.Count - 1, phase) + : Math.Max(0, route.Count - 1 - Math.Min(route.Count - 1, phase - oneWayMinutes)); + var position = route[routeIndex]; + (int q, int r) destination = isDelivering ? (home.Q, home.R) : (building.Q, building.R); + + await _units.UpdateOneAsync( + x => x.Id == building.WorkerUnitId && x.Username == username, + Builders.Update + .Set(x => x.Q, position.q) + .Set(x => x.R, position.r) + .Set(x => x.Status, isDelivering ? "DeliveringToTownHall" : "ReturningToWork") + .Set(x => x.WorksiteQ, building.Q) + .Set(x => x.WorksiteR, building.R) + .Set(x => x.DestinationQ, destination.q) + .Set(x => x.DestinationR, destination.r) + .Set(x => x.CarryingResourceType, isDelivering ? resourceType : (string?)null) + .Set(x => x.CarryingResourceAmount, isDelivering ? Math.Min(carryCapacity, remainingStored) : 0) + .Set(x => x.MovementStartedAtUtc, now) + .Set(x => x.MovementCompletesAtUtc, now.AddMinutes(1)) + .Set(x => x.UpdatedAtUtc, now)); + } + private async Task CalculateFoodProductionPerMinuteAsync(string username) { var townHallFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodPerMinute, _townHallFoodPerMinute); @@ -514,6 +616,17 @@ public class ResourceService return (hasTownHall ? townHallFoodPerMinute : 0) + checked((int)farms) * farmFoodPerMinute; } + private async Task CalculateDirectFoodProductionPerMinuteAsync(string username) + { + var townHallFoodPerMinute = await _settings.GetNonNegativeIntAsync(GameSettingsService.TownHallFoodPerMinute, _townHallFoodPerMinute); + var hasTownHall = await _locations.Find(x => + x.OwnerUsername == username && + x.Kind == "Home") + .AnyAsync(); + + return hasTownHall ? townHallFoodPerMinute : 0; + } + private async Task CompletePendingWorkerAssignmentsAsync(string username, DateTime now) { var movingWorkers = await _units.Find(x => @@ -543,6 +656,7 @@ public class ResourceService var currentQ = worker.Q; var currentR = worker.R; var elapsedSteps = Math.Max(1, (int)Math.Floor((now - worker.MovementCompletesAtUtc!.Value).TotalMinutes) + 1); + var usedSteps = 0; var movementBlocked = false; while (elapsedSteps > 0 && HexDistance(currentQ, currentR, destinationQ, destinationR) > 1) @@ -566,6 +680,7 @@ public class ResourceService currentQ = nextStep.Value.q; currentR = nextStep.Value.r; elapsedSteps--; + usedSteps++; } if (movementBlocked) @@ -583,6 +698,7 @@ public class ResourceService continue; } + var assignedAt = worker.MovementCompletesAtUtc.Value.AddMinutes(Math.Max(0, usedSteps - 1)); var location = await _locations.FindOneAndUpdateAsync( x => x.OwnerUsername == username && x.Q == destinationQ && @@ -593,7 +709,8 @@ public class ResourceService Builders.Update .Set(x => x.WorkerUnitId, worker.Id) .Set(x => x.WorkerUnitType, "Peasant") - .Set(x => x.LastResourceGeneratedAtUtc, now) + .Set(x => x.LastResourceGeneratedAtUtc, assignedAt) + .Set(x => x.DeliveryCycleStartedAtUtc, assignedAt) .Set(x => x.UpdatedAtUtc, now), new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); @@ -675,6 +792,25 @@ public class ResourceService yield return (q, r + 1); } + private static List<(int q, int r)> DirectRoute((int q, int r) start, (int q, int r) target) + { + var route = new List<(int q, int r)> { start }; + var current = start; + var guard = 0; + while (current != target && guard < 128) + { + current = NeighborCoordinates(current.q, current.r) + .OrderBy(x => HexDistance(x.q, x.r, target.q, target.r)) + .ThenBy(x => x.q) + .ThenBy(x => x.r) + .First(); + route.Add(current); + guard++; + } + + return route; + } + private static int HexDistance(int q1, int r1, int q2, int r2) { var s1 = -q1 - r1; @@ -713,6 +849,7 @@ public class ResourceService Builders.Update .Unset(x => x.WorkerUnitId) .Unset(x => x.WorkerUnitType) + .Unset(x => x.DeliveryCycleStartedAtUtc) .Set(x => x.UpdatedAtUtc, now)); await _units.DeleteManyAsync(x => x.Username == username &&