Recommiting glbs for LFS
Deploy Promiscuity Auth API / deploy (push) Successful in 1m59s
Deploy Promiscuity Character API / deploy (push) Successful in 1m16s
Deploy Promiscuity Inventory API / deploy (push) Has been cancelled
Deploy Promiscuity Locations API / deploy (push) Has been cancelled
Deploy Promiscuity Mail API / deploy (push) Has been cancelled
Deploy Promiscuity World API / deploy (push) Has been cancelled
Deploy Promiscuity Crafting API / deploy (push) Has been cancelled
k8s smoke test / test (push) Has been cancelled

This commit is contained in:
2026-05-12 15:34:12 -05:00
parent a9e546a121
commit fecbefc15c
141 changed files with 7099 additions and 2601 deletions
@@ -99,7 +99,7 @@ public class CraftingStore
{
var recipes = await _recipes.Find(r => r.Enabled).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
var items = await GetCharacterItemsAsync(character.CharacterId);
var itemTotals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
var itemTotals = GetCraftableItemTotals(items);
var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
return recipes.Select(recipe =>
@@ -126,7 +126,7 @@ public class CraftingStore
return new CraftAttemptResult { Status = CraftStatus.MissingStation };
var items = await GetCharacterItemsAsync(character.CharacterId);
var totals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
var totals = GetCraftableItemTotals(items);
var missing = GetMissingRequirements(recipe, totals, location, craftCount);
if (missing.Count > 0)
return new CraftAttemptResult { Status = CraftStatus.MissingInputs, MissingRequirements = missing };
@@ -139,6 +139,10 @@ public class CraftingStore
return new CraftAttemptResult { Status = CraftStatus.InvalidRecipe, MissingRequirements = [$"Missing item definition for output '{output.ItemKey}'"] };
}
var capacityError = GetOutputCapacityError(recipe, craftCount, items, definitionMap);
if (capacityError is not null)
return new CraftAttemptResult { Status = CraftStatus.InventoryFull, MissingRequirements = [capacityError] };
foreach (var input in recipe.Inputs)
await ConsumeItemKeyAsync(character, input.ItemKey, input.Quantity * craftCount);
@@ -157,6 +161,12 @@ public class CraftingStore
private async Task<List<InventoryItemDocument>> GetCharacterItemsAsync(string characterId) =>
await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == characterId).SortBy(i => i.Slot).ThenBy(i => i.ItemKey).ToListAsync();
private static Dictionary<string, int> GetCraftableItemTotals(IEnumerable<InventoryItemDocument> items) =>
items
.Where(i => i.EquippedSlot is null)
.GroupBy(i => i.ItemKey)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
private async Task<LocationDocument?> GetLocationAtCoordAsync(int x, int y) =>
await _locations.Find(l => l.Coord.X == x && l.Coord.Y == y).FirstOrDefaultAsync();
@@ -216,6 +226,74 @@ public class CraftingStore
return string.Equals(recipe.StationType, stationType, StringComparison.Ordinal);
}
private static string? GetOutputCapacityError(CraftingRecipe recipe, int craftCount, List<InventoryItemDocument> currentItems, IReadOnlyDictionary<string, ItemDefinitionDocument> definitions)
{
var simulatedItems = currentItems
.Select(item => new SimulatedInventoryItem
{
ItemKey = item.ItemKey,
Quantity = item.Quantity,
Slot = item.Slot,
EquippedSlot = item.EquippedSlot,
CreatedUtc = item.CreatedUtc
})
.ToList();
foreach (var input in recipe.Inputs)
SimulateConsume(simulatedItems, input.ItemKey, input.Quantity * craftCount);
var usedSlots = simulatedItems
.Where(i => i.Slot.HasValue)
.Select(i => i.Slot!.Value)
.ToHashSet();
var openSlots = Enumerable.Range(0, InventorySlotCount).Count(slot => !usedSlots.Contains(slot));
foreach (var outputGroup in recipe.Outputs.GroupBy(output => output.ItemKey, StringComparer.Ordinal))
{
var itemKey = outputGroup.Key;
var requiredQuantity = outputGroup.Sum(output => output.Quantity * craftCount);
var definition = definitions[itemKey];
if (definition.Stackable)
{
var existingSpace = simulatedItems
.Where(i => i.EquippedSlot is null && i.Slot.HasValue && i.ItemKey == itemKey)
.Sum(i => Math.Max(0, definition.MaxStackSize - i.Quantity));
var remaining = Math.Max(0, requiredQuantity - existingSpace);
var slotsNeeded = (int)Math.Ceiling(remaining / (double)Math.Max(1, definition.MaxStackSize));
if (slotsNeeded > openSlots)
return $"Not enough inventory space for output '{itemKey}'";
openSlots -= slotsNeeded;
continue;
}
if (requiredQuantity > openSlots)
return $"Not enough inventory space for output '{itemKey}'";
openSlots -= requiredQuantity;
}
return null;
}
private static void SimulateConsume(List<SimulatedInventoryItem> items, string itemKey, int quantity)
{
var remaining = quantity;
foreach (var item in items
.Where(i => i.EquippedSlot is null && i.ItemKey == itemKey)
.OrderBy(i => i.Slot)
.ThenBy(i => i.CreatedUtc))
{
if (remaining <= 0)
break;
var consumed = Math.Min(remaining, item.Quantity);
item.Quantity -= consumed;
remaining -= consumed;
}
items.RemoveAll(i => i.Quantity <= 0);
}
private async Task ConsumeItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity)
{
var remaining = quantity;
@@ -302,7 +380,7 @@ public class CraftingStore
{
var items = await GetCharacterItemsAsync(characterId);
var used = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
for (var slot = 0; slot < 6; slot++)
for (var slot = 0; slot < InventorySlotCount; slot++)
{
if (!used.Contains(slot))
return slot;
@@ -344,7 +422,23 @@ public class CraftingStore
RecipeNotFound,
MissingInputs,
MissingStation,
InvalidRecipe
InvalidRecipe,
InventoryFull
}
private const int InventorySlotCount = 6;
private class SimulatedInventoryItem
{
public string ItemKey { get; set; } = string.Empty;
public int Quantity { get; set; }
public int? Slot { get; set; }
public string? EquippedSlot { get; set; }
public DateTime CreatedUtc { get; set; }
}
[BsonIgnoreExtraElements]