Adding inventory stacking and overflow to gather mechanic
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 47s
Deploy Promiscuity Character API / deploy (push) Successful in 44s
Deploy Promiscuity Inventory API / deploy (push) Successful in 58s
Deploy Promiscuity Locations API / deploy (push) Successful in 58s
k8s smoke test / test (push) Successful in 9s
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 47s
Deploy Promiscuity Character API / deploy (push) Successful in 44s
Deploy Promiscuity Inventory API / deploy (push) Successful in 58s
Deploy Promiscuity Locations API / deploy (push) Successful in 58s
k8s smoke test / test (push) Successful in 9s
This commit is contained in:
parent
69ff204c5d
commit
b8ce13f1d2
@ -2,8 +2,9 @@ extends Node3D
|
|||||||
|
|
||||||
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
||||||
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
||||||
|
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
||||||
|
|
||||||
@export var tile_size := 16.0
|
@export var tile_size := 8.0
|
||||||
@export var block_height := 1.0
|
@export var block_height := 1.0
|
||||||
@export_range(1, 8, 1) var tile_radius := 3
|
@export_range(1, 8, 1) var tile_radius := 3
|
||||||
@export var tracked_node_path: NodePath
|
@export var tracked_node_path: NodePath
|
||||||
@ -154,6 +155,7 @@ func _spawn_tile(coord: Vector2i, location_data: Dictionary) -> void:
|
|||||||
if show_tile_labels:
|
if show_tile_labels:
|
||||||
tile_root.add_child(_create_tile_label(String(location_data.get("name", ""))))
|
tile_root.add_child(_create_tile_label(String(location_data.get("name", ""))))
|
||||||
_update_tile_object(tile_root, location_data)
|
_update_tile_object(tile_root, location_data)
|
||||||
|
_update_tile_inventory(tile_root, location_data)
|
||||||
|
|
||||||
_tile_nodes[coord] = tile_root
|
_tile_nodes[coord] = tile_root
|
||||||
|
|
||||||
@ -169,41 +171,71 @@ func _update_tile(coord: Vector2i, location_data: Dictionary) -> void:
|
|||||||
label.text = String(location_data.get("name", ""))
|
label.text = String(location_data.get("name", ""))
|
||||||
|
|
||||||
_update_tile_object(tile_root, location_data)
|
_update_tile_object(tile_root, location_data)
|
||||||
|
_update_tile_inventory(tile_root, location_data)
|
||||||
|
|
||||||
|
|
||||||
func _update_tile_object(tile_root: Node3D, location_data: Dictionary) -> void:
|
func _update_tile_object(tile_root: Node3D, location_data: Dictionary) -> void:
|
||||||
var existing := tile_root.get_node_or_null("LocationObject")
|
|
||||||
if existing:
|
|
||||||
existing.queue_free()
|
|
||||||
|
|
||||||
var object_data_variant: Variant = location_data.get("locationObject", {})
|
var object_data_variant: Variant = location_data.get("locationObject", {})
|
||||||
if typeof(object_data_variant) != TYPE_DICTIONARY:
|
if typeof(object_data_variant) != TYPE_DICTIONARY:
|
||||||
|
var existing_missing := tile_root.get_node_or_null("LocationObject")
|
||||||
|
if existing_missing:
|
||||||
|
existing_missing.queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
var object_data := object_data_variant as Dictionary
|
var object_data := object_data_variant as Dictionary
|
||||||
if object_data.is_empty():
|
if object_data.is_empty():
|
||||||
|
var existing_empty := tile_root.get_node_or_null("LocationObject")
|
||||||
|
if existing_empty:
|
||||||
|
existing_empty.queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
var object_root := Node3D.new()
|
var object_root := tile_root.get_node_or_null("LocationObject") as Node3D
|
||||||
|
if object_root == null:
|
||||||
|
object_root = Node3D.new()
|
||||||
object_root.name = "LocationObject"
|
object_root.name = "LocationObject"
|
||||||
object_root.position = Vector3(0.0, (block_height * 0.5) + 0.6, 0.0)
|
object_root.position = Vector3(0.0, (block_height * 0.5) + 0.6, 0.0)
|
||||||
tile_root.add_child(object_root)
|
tile_root.add_child(object_root)
|
||||||
|
|
||||||
var object_mesh := MeshInstance3D.new()
|
var object_mesh := object_root.get_node_or_null("ObjectMesh") as MeshInstance3D
|
||||||
|
if object_mesh == null:
|
||||||
|
object_mesh = MeshInstance3D.new()
|
||||||
object_mesh.name = "ObjectMesh"
|
object_mesh.name = "ObjectMesh"
|
||||||
object_mesh.mesh = SphereMesh.new()
|
object_mesh.mesh = SphereMesh.new()
|
||||||
object_mesh.scale = Vector3(0.6, 0.4, 0.6)
|
object_mesh.scale = Vector3(0.6, 0.4, 0.6)
|
||||||
object_mesh.material_override = _create_object_material(String(object_data.get("objectKey", "")))
|
|
||||||
object_root.add_child(object_mesh)
|
object_root.add_child(object_mesh)
|
||||||
|
object_mesh.material_override = _create_object_material(String(object_data.get("objectKey", "")))
|
||||||
|
|
||||||
var object_label := Label3D.new()
|
var object_label := object_root.get_node_or_null("ObjectLabel") as Label3D
|
||||||
|
if object_label == null:
|
||||||
|
object_label = Label3D.new()
|
||||||
object_label.name = "ObjectLabel"
|
object_label.name = "ObjectLabel"
|
||||||
object_label.text = _build_object_label(object_data)
|
|
||||||
object_label.position = Vector3(0.0, 0.6, 0.0)
|
object_label.position = Vector3(0.0, 0.6, 0.0)
|
||||||
object_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
object_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
object_label.pixel_size = 0.01
|
object_label.pixel_size = 0.01
|
||||||
object_label.outline_size = 10
|
object_label.outline_size = 10
|
||||||
object_root.add_child(object_label)
|
object_root.add_child(object_label)
|
||||||
|
object_label.text = _build_object_label(object_data)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_tile_inventory(tile_root: Node3D, location_data: Dictionary) -> void:
|
||||||
|
var floor_items: Array = location_data.get("floorItems", [])
|
||||||
|
var existing_label := tile_root.get_node_or_null("FloorInventoryLabel") as Label3D
|
||||||
|
if floor_items.is_empty():
|
||||||
|
if existing_label:
|
||||||
|
existing_label.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
if existing_label == null:
|
||||||
|
existing_label = Label3D.new()
|
||||||
|
existing_label.name = "FloorInventoryLabel"
|
||||||
|
existing_label.position = Vector3(0.0, (block_height * 0.5) + 1.25, 0.0)
|
||||||
|
existing_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
existing_label.pixel_size = 0.01
|
||||||
|
existing_label.outline_size = 10
|
||||||
|
existing_label.modulate = Color(1.0, 0.95, 0.75, 1.0)
|
||||||
|
tile_root.add_child(existing_label)
|
||||||
|
|
||||||
|
existing_label.text = _build_floor_inventory_label(floor_items)
|
||||||
|
|
||||||
|
|
||||||
func _create_tile_border() -> MeshInstance3D:
|
func _create_tile_border() -> MeshInstance3D:
|
||||||
@ -284,6 +316,28 @@ func _build_object_label(object_data: Dictionary) -> String:
|
|||||||
return "%s x%d" % [object_name, remaining_quantity]
|
return "%s x%d" % [object_name, remaining_quantity]
|
||||||
|
|
||||||
|
|
||||||
|
func _build_floor_inventory_label(floor_items: Array) -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
for entry in floor_items:
|
||||||
|
if typeof(entry) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var item := entry as Dictionary
|
||||||
|
parts.append("%s x%d" % [
|
||||||
|
String(item.get("itemKey", "")).strip_edges(),
|
||||||
|
int(item.get("quantity", 0))
|
||||||
|
])
|
||||||
|
if parts.size() >= 3:
|
||||||
|
break
|
||||||
|
|
||||||
|
if parts.is_empty():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
var label := "Floor: %s" % ", ".join(parts)
|
||||||
|
if floor_items.size() > parts.size():
|
||||||
|
label += " ..."
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
||||||
if _known_locations.has(coord):
|
if _known_locations.has(coord):
|
||||||
return
|
return
|
||||||
@ -291,7 +345,8 @@ func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
|||||||
"id": "",
|
"id": "",
|
||||||
"name": _selected_location_name(coord),
|
"name": _selected_location_name(coord),
|
||||||
"biomeKey": "plains",
|
"biomeKey": "plains",
|
||||||
"locationObject": {}
|
"locationObject": {},
|
||||||
|
"floorItems": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -367,7 +422,8 @@ func _load_existing_locations() -> void:
|
|||||||
"id": String(location.get("id", "")).strip_edges(),
|
"id": String(location.get("id", "")).strip_edges(),
|
||||||
"name": location_name,
|
"name": location_name,
|
||||||
"biomeKey": String(location.get("biomeKey", "plains")).strip_edges(),
|
"biomeKey": String(location.get("biomeKey", "plains")).strip_edges(),
|
||||||
"locationObject": _parse_location_object(location.get("locationObject", {}))
|
"locationObject": _parse_location_object(location.get("locationObject", {})),
|
||||||
|
"floorItems": []
|
||||||
}
|
}
|
||||||
loaded_count += 1
|
loaded_count += 1
|
||||||
|
|
||||||
@ -375,6 +431,8 @@ func _load_existing_locations() -> void:
|
|||||||
if loaded_count == 0:
|
if loaded_count == 0:
|
||||||
push_warning("Visible locations request succeeded but returned 0 locations for character %s." % _character_id)
|
push_warning("Visible locations request succeeded but returned 0 locations for character %s." % _character_id)
|
||||||
|
|
||||||
|
await _load_visible_location_inventories()
|
||||||
|
|
||||||
_locations_loaded = true
|
_locations_loaded = true
|
||||||
_locations_refresh_in_flight = false
|
_locations_refresh_in_flight = false
|
||||||
_rebuild_tiles(_center_coord)
|
_rebuild_tiles(_center_coord)
|
||||||
@ -469,6 +527,7 @@ func _interact_with_location_async(location_id: String, object_id: String) -> vo
|
|||||||
|
|
||||||
var interaction := parsed as Dictionary
|
var interaction := parsed as Dictionary
|
||||||
_apply_interaction_result(location_id, interaction)
|
_apply_interaction_result(location_id, interaction)
|
||||||
|
await _refresh_location_inventory(location_id)
|
||||||
_interact_in_flight = false
|
_interact_in_flight = false
|
||||||
|
|
||||||
|
|
||||||
@ -564,6 +623,83 @@ func _parse_location_object(value: Variant) -> Dictionary:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_floor_inventory_items(value: Variant) -> Array:
|
||||||
|
var items: Array = []
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return items
|
||||||
|
|
||||||
|
for entry in value:
|
||||||
|
if typeof(entry) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var item := entry as Dictionary
|
||||||
|
items.append({
|
||||||
|
"itemKey": String(item.get("itemKey", "")).strip_edges(),
|
||||||
|
"quantity": int(item.get("quantity", 0)),
|
||||||
|
"slot": item.get("slot", null)
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
func _load_visible_location_inventories() -> void:
|
||||||
|
for coord_variant in _known_locations.keys():
|
||||||
|
var coord: Vector2i = coord_variant
|
||||||
|
var location_data: Dictionary = _known_locations[coord]
|
||||||
|
var location_id := String(location_data.get("id", "")).strip_edges()
|
||||||
|
if location_id.is_empty():
|
||||||
|
continue
|
||||||
|
var floor_items := await _fetch_location_inventory(location_id)
|
||||||
|
location_data["floorItems"] = floor_items
|
||||||
|
_known_locations[coord] = location_data
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_location_inventory(location_id: String) -> void:
|
||||||
|
if location_id.is_empty():
|
||||||
|
return
|
||||||
|
var floor_items := await _fetch_location_inventory(location_id)
|
||||||
|
for coord_variant in _known_locations.keys():
|
||||||
|
var coord: Vector2i = coord_variant
|
||||||
|
var location_data: Dictionary = _known_locations[coord]
|
||||||
|
if String(location_data.get("id", "")).strip_edges() != location_id:
|
||||||
|
continue
|
||||||
|
location_data["floorItems"] = floor_items
|
||||||
|
_known_locations[coord] = location_data
|
||||||
|
_update_tile(coord, location_data)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
func _fetch_location_inventory(location_id: String) -> Array:
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
|
||||||
|
var err := request.request("%s/by-owner/location/%s" % [INVENTORY_API_URL, location_id], headers, HTTPClient.METHOD_GET)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
push_warning("Failed to request floor inventory for location %s: %s" % [location_id, err])
|
||||||
|
return []
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
var response_body: String = result[3].get_string_from_utf8()
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
push_warning("Failed to load floor inventory for location %s (%s/%s): %s" % [location_id, result_code, response_code, response_body])
|
||||||
|
return []
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
return []
|
||||||
|
|
||||||
|
var payload := parsed as Dictionary
|
||||||
|
return _parse_floor_inventory_items(payload.get("items", []))
|
||||||
|
|
||||||
|
|
||||||
func _get_biome_material(tile: MeshInstance3D, biome_key: String) -> Material:
|
func _get_biome_material(tile: MeshInstance3D, biome_key: String) -> Material:
|
||||||
var normalized_biome := biome_key if not biome_key.is_empty() else "plains"
|
var normalized_biome := biome_key if not biome_key.is_empty() else "plains"
|
||||||
if _biome_materials.has(normalized_biome):
|
if _biome_materials.has(normalized_biome):
|
||||||
|
|||||||
@ -124,12 +124,15 @@ public class InventoryController : ControllerBase
|
|||||||
if (definition is null)
|
if (definition is null)
|
||||||
return BadRequest("Unknown itemKey");
|
return BadRequest("Unknown itemKey");
|
||||||
|
|
||||||
var items = await _inventory.GrantAsync(access, req, definition);
|
var grant = await _inventory.GrantAsync(access, req, definition);
|
||||||
return Ok(new InventoryOwnerResponse
|
return Ok(new InventoryOwnerResponse
|
||||||
{
|
{
|
||||||
OwnerType = access.OwnerType,
|
OwnerType = access.OwnerType,
|
||||||
OwnerId = access.OwnerId,
|
OwnerId = access.OwnerId,
|
||||||
Items = items.Select(InventoryItemResponse.FromModel).ToList()
|
RequestedQuantity = grant.RequestedQuantity,
|
||||||
|
GrantedQuantity = grant.GrantedQuantity,
|
||||||
|
OverflowQuantity = grant.OverflowQuantity,
|
||||||
|
Items = grant.Items.Select(InventoryItemResponse.FromModel).ToList()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,11 @@ public class InventoryOwnerResponse
|
|||||||
|
|
||||||
public string OwnerId { get; set; } = string.Empty;
|
public string OwnerId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int RequestedQuantity { get; set; }
|
||||||
|
|
||||||
|
public int GrantedQuantity { get; set; }
|
||||||
|
|
||||||
|
public int OverflowQuantity { get; set; }
|
||||||
|
|
||||||
public List<InventoryItemResponse> Items { get; set; } = [];
|
public List<InventoryItemResponse> Items { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ public class InventoryStore
|
|||||||
{
|
{
|
||||||
private const string CharacterOwnerType = "character";
|
private const string CharacterOwnerType = "character";
|
||||||
private const string LocationOwnerType = "location";
|
private const string LocationOwnerType = "location";
|
||||||
|
private const int CharacterInventorySlotCount = 6;
|
||||||
private const string OwnerIndexName = "owner_type_1_owner_id_1";
|
private const string OwnerIndexName = "owner_type_1_owner_id_1";
|
||||||
private const string SlotIndexName = "owner_type_1_owner_id_1_slot_1";
|
private const string SlotIndexName = "owner_type_1_owner_id_1_slot_1";
|
||||||
private const string EquippedSlotIndexName = "owner_type_1_owner_id_1_equipped_slot_1";
|
private const string EquippedSlotIndexName = "owner_type_1_owner_id_1_equipped_slot_1";
|
||||||
@ -20,6 +21,8 @@ public class InventoryStore
|
|||||||
private readonly IMongoClient _client;
|
private readonly IMongoClient _client;
|
||||||
private readonly string _dbName;
|
private readonly string _dbName;
|
||||||
|
|
||||||
|
public sealed record GrantResult(List<InventoryItem> Items, int RequestedQuantity, int GrantedQuantity, int OverflowQuantity);
|
||||||
|
|
||||||
public InventoryStore(IConfiguration cfg)
|
public InventoryStore(IConfiguration cfg)
|
||||||
{
|
{
|
||||||
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||||
@ -131,31 +134,44 @@ public class InventoryStore
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<InventoryItem>> GrantAsync(OwnerAccessResult owner, GrantInventoryItemRequest req, ItemDefinition definition)
|
public async Task<GrantResult> GrantAsync(OwnerAccessResult owner, GrantInventoryItemRequest req, ItemDefinition definition)
|
||||||
{
|
{
|
||||||
var normalizedKey = NormalizeItemKey(req.ItemKey);
|
var normalizedKey = NormalizeItemKey(req.ItemKey);
|
||||||
|
var remaining = req.Quantity;
|
||||||
if (definition.Stackable)
|
if (definition.Stackable)
|
||||||
{
|
{
|
||||||
var remaining = req.Quantity;
|
|
||||||
var targetSlot = req.PreferredSlot;
|
var targetSlot = req.PreferredSlot;
|
||||||
while (remaining > 0)
|
var existingStacks = await _items.Find(i =>
|
||||||
{
|
i.OwnerType == owner.OwnerType &&
|
||||||
var slot = targetSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
|
i.OwnerId == owner.OwnerId &&
|
||||||
var existing = await FindStackAsync(owner.OwnerType, owner.OwnerId, normalizedKey, slot);
|
i.ItemKey == normalizedKey &&
|
||||||
if (existing is not null)
|
i.EquippedSlot == null &&
|
||||||
|
i.Slot != null)
|
||||||
|
.SortBy(i => i.Slot)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var existing in existingStacks)
|
||||||
{
|
{
|
||||||
|
if (remaining <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
var availableSpace = definition.MaxStackSize - existing.Quantity;
|
var availableSpace = definition.MaxStackSize - existing.Quantity;
|
||||||
if (availableSpace > 0)
|
if (availableSpace <= 0)
|
||||||
{
|
continue;
|
||||||
|
|
||||||
var added = Math.Min(remaining, availableSpace);
|
var added = Math.Min(remaining, availableSpace);
|
||||||
existing.Quantity += added;
|
existing.Quantity += added;
|
||||||
existing.UpdatedUtc = DateTime.UtcNow;
|
existing.UpdatedUtc = DateTime.UtcNow;
|
||||||
await ReplaceItemAsync(existing);
|
await ReplaceItemAsync(existing);
|
||||||
remaining -= added;
|
remaining -= added;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
while (remaining > 0)
|
||||||
{
|
{
|
||||||
|
var slot = targetSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
|
||||||
|
if (slot is null)
|
||||||
|
break;
|
||||||
|
|
||||||
var stackQuantity = Math.Min(remaining, definition.MaxStackSize);
|
var stackQuantity = Math.Min(remaining, definition.MaxStackSize);
|
||||||
await InsertItemAsync(new InventoryItem
|
await InsertItemAsync(new InventoryItem
|
||||||
{
|
{
|
||||||
@ -164,20 +180,24 @@ public class InventoryStore
|
|||||||
OwnerType = owner.OwnerType,
|
OwnerType = owner.OwnerType,
|
||||||
OwnerId = owner.OwnerId,
|
OwnerId = owner.OwnerId,
|
||||||
OwnerUserId = owner.OwnerUserId,
|
OwnerUserId = owner.OwnerUserId,
|
||||||
Slot = slot
|
Slot = slot.Value
|
||||||
});
|
});
|
||||||
remaining -= stackQuantity;
|
remaining -= stackQuantity;
|
||||||
}
|
|
||||||
|
|
||||||
targetSlot = null;
|
targetSlot = null;
|
||||||
}
|
}
|
||||||
return await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
|
|
||||||
|
var stackItems = await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
|
||||||
|
return new GrantResult(stackItems, req.Quantity, req.Quantity - remaining, remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextPreferredSlot = req.PreferredSlot;
|
var nextPreferredSlot = req.PreferredSlot;
|
||||||
for (var index = 0; index < req.Quantity; index += 1)
|
while (remaining > 0)
|
||||||
{
|
{
|
||||||
var slot = nextPreferredSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
|
var slot = nextPreferredSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
|
||||||
|
if (slot is null)
|
||||||
|
break;
|
||||||
|
|
||||||
await InsertItemAsync(new InventoryItem
|
await InsertItemAsync(new InventoryItem
|
||||||
{
|
{
|
||||||
ItemKey = normalizedKey,
|
ItemKey = normalizedKey,
|
||||||
@ -185,11 +205,13 @@ public class InventoryStore
|
|||||||
OwnerType = owner.OwnerType,
|
OwnerType = owner.OwnerType,
|
||||||
OwnerId = owner.OwnerId,
|
OwnerId = owner.OwnerId,
|
||||||
OwnerUserId = owner.OwnerUserId,
|
OwnerUserId = owner.OwnerUserId,
|
||||||
Slot = slot
|
Slot = slot.Value
|
||||||
});
|
});
|
||||||
|
remaining -= 1;
|
||||||
nextPreferredSlot = null;
|
nextPreferredSlot = null;
|
||||||
}
|
}
|
||||||
return await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
|
var nonStackItems = await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
|
||||||
|
return new GrantResult(nonStackItems, req.Quantity, req.Quantity - remaining, remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<InventoryMutationResult> MoveAsync(OwnerAccessResult owner, MoveInventoryItemRequest req)
|
public async Task<InventoryMutationResult> MoveAsync(OwnerAccessResult owner, MoveInventoryItemRequest req)
|
||||||
@ -298,7 +320,12 @@ public class InventoryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
var toSlot = req.ToSlot ?? await FindFirstOpenSlotAsync(toOwner.OwnerType, toOwner.OwnerId, session);
|
var toSlot = req.ToSlot ?? await FindFirstOpenSlotAsync(toOwner.OwnerType, toOwner.OwnerId, session);
|
||||||
var target = await FindItemBySlotAsync(toOwner.OwnerType, toOwner.OwnerId, toSlot, session);
|
if (toSlot is null)
|
||||||
|
{
|
||||||
|
await session.AbortTransactionAsync();
|
||||||
|
return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
|
||||||
|
}
|
||||||
|
var target = await FindItemBySlotAsync(toOwner.OwnerType, toOwner.OwnerId, toSlot.Value, session);
|
||||||
if (target is not null && !CanMerge(item, target, definition))
|
if (target is not null && !CanMerge(item, target, definition))
|
||||||
{
|
{
|
||||||
await session.AbortTransactionAsync();
|
await session.AbortTransactionAsync();
|
||||||
@ -451,12 +478,14 @@ public class InventoryStore
|
|||||||
return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };
|
return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };
|
||||||
|
|
||||||
var slot = preferredSlot ?? await FindFirstOpenSlotAsync(item.OwnerType, item.OwnerId);
|
var slot = preferredSlot ?? await FindFirstOpenSlotAsync(item.OwnerType, item.OwnerId);
|
||||||
var existing = await FindItemBySlotAsync(item.OwnerType, item.OwnerId, slot);
|
if (slot is null)
|
||||||
|
return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
|
||||||
|
var existing = await FindItemBySlotAsync(item.OwnerType, item.OwnerId, slot.Value);
|
||||||
if (existing is not null && existing.Id != item.Id)
|
if (existing is not null && existing.Id != item.Id)
|
||||||
return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
|
return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
|
||||||
|
|
||||||
item.EquippedSlot = null;
|
item.EquippedSlot = null;
|
||||||
item.Slot = slot;
|
item.Slot = slot.Value;
|
||||||
item.UpdatedUtc = DateTime.UtcNow;
|
item.UpdatedUtc = DateTime.UtcNow;
|
||||||
await ReplaceItemAsync(item);
|
await ReplaceItemAsync(item);
|
||||||
|
|
||||||
@ -480,16 +509,19 @@ public class InventoryStore
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> FindFirstOpenSlotAsync(string ownerType, string ownerId, IClientSessionHandle? session = null)
|
private async Task<int?> FindFirstOpenSlotAsync(string ownerType, string ownerId, IClientSessionHandle? session = null)
|
||||||
{
|
{
|
||||||
var items = session is null
|
var items = session is null
|
||||||
? await GetByOwnerAsync(ownerType, ownerId)
|
? await GetByOwnerAsync(ownerType, ownerId)
|
||||||
: await _items.Find(session, i => i.OwnerType == ownerType && i.OwnerId == ownerId).ToListAsync();
|
: await _items.Find(session, i => i.OwnerType == ownerType && i.OwnerId == ownerId).ToListAsync();
|
||||||
|
|
||||||
var usedSlots = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
|
var usedSlots = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
|
||||||
|
var maxSlotCount = GetMaxSlotCount(ownerType);
|
||||||
var slot = 0;
|
var slot = 0;
|
||||||
while (usedSlots.Contains(slot))
|
while (usedSlots.Contains(slot))
|
||||||
slot += 1;
|
slot += 1;
|
||||||
|
if (maxSlotCount.HasValue && slot >= maxSlotCount.Value)
|
||||||
|
return null;
|
||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,6 +572,11 @@ public class InventoryStore
|
|||||||
target.EquippedSlot is null &&
|
target.EquippedSlot is null &&
|
||||||
definition.Stackable;
|
definition.Stackable;
|
||||||
|
|
||||||
|
private static int? GetMaxSlotCount(string ownerType) =>
|
||||||
|
string.Equals(ownerType, CharacterOwnerType, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? CharacterInventorySlotCount
|
||||||
|
: null;
|
||||||
|
|
||||||
private void EnsureIndexes()
|
private void EnsureIndexes()
|
||||||
{
|
{
|
||||||
_items.Indexes.CreateOne(new CreateIndexModel<InventoryItem>(
|
_items.Indexes.CreateOne(new CreateIndexModel<InventoryItem>(
|
||||||
|
|||||||
@ -180,6 +180,64 @@ public class LocationsController : ControllerBase
|
|||||||
return StatusCode((int)response.StatusCode, responseBody);
|
return StatusCode((int)response.StatusCode, responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var characterGrantedQuantity = interact.QuantityGranted;
|
||||||
|
var floorGrantedQuantity = 0;
|
||||||
|
var overflowQuantity = 0;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(responseBody))
|
||||||
|
{
|
||||||
|
using var grantJson = JsonDocument.Parse(responseBody);
|
||||||
|
if (grantJson.RootElement.TryGetProperty("grantedQuantity", out var grantedElement) && grantedElement.ValueKind == JsonValueKind.Number)
|
||||||
|
characterGrantedQuantity = grantedElement.GetInt32();
|
||||||
|
if (grantJson.RootElement.TryGetProperty("overflowQuantity", out var overflowElement) && overflowElement.ValueKind == JsonValueKind.Number)
|
||||||
|
overflowQuantity = overflowElement.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowQuantity > 0)
|
||||||
|
{
|
||||||
|
var floorGrantBody = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
itemKey = interact.ItemKey,
|
||||||
|
quantity = overflowQuantity
|
||||||
|
});
|
||||||
|
|
||||||
|
using var floorRequest = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post,
|
||||||
|
$"{inventoryBaseUrl}/api/inventory/by-owner/location/{id}/grant");
|
||||||
|
floorRequest.Content = new StringContent(floorGrantBody, Encoding.UTF8, "application/json");
|
||||||
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
|
floorRequest.Headers.Authorization = AuthenticationHeaderValue.Parse(token);
|
||||||
|
|
||||||
|
using var floorResponse = await client.SendAsync(floorRequest);
|
||||||
|
var floorResponseBody = await floorResponse.Content.ReadAsStringAsync();
|
||||||
|
if (!floorResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Character inventory overflow could not be redirected to location inventory. Location {LocationId}, character {CharacterId}, item {ItemKey}, quantity {Quantity}, response {StatusCode}: {Body}",
|
||||||
|
id,
|
||||||
|
req.CharacterId,
|
||||||
|
interact.ItemKey,
|
||||||
|
overflowQuantity,
|
||||||
|
(int)floorResponse.StatusCode,
|
||||||
|
floorResponseBody
|
||||||
|
);
|
||||||
|
return StatusCode((int)floorResponse.StatusCode, floorResponseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(floorResponseBody))
|
||||||
|
{
|
||||||
|
using var floorJson = JsonDocument.Parse(floorResponseBody);
|
||||||
|
if (floorJson.RootElement.TryGetProperty("grantedQuantity", out var floorGrantedElement) && floorGrantedElement.ValueKind == JsonValueKind.Number)
|
||||||
|
floorGrantedQuantity = floorGrantedElement.GetInt32();
|
||||||
|
else
|
||||||
|
floorGrantedQuantity = overflowQuantity;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
floorGrantedQuantity = overflowQuantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(new InteractLocationObjectResponse
|
return Ok(new InteractLocationObjectResponse
|
||||||
{
|
{
|
||||||
LocationId = id,
|
LocationId = id,
|
||||||
@ -188,6 +246,8 @@ public class LocationsController : ControllerBase
|
|||||||
ObjectType = interact.ObjectType,
|
ObjectType = interact.ObjectType,
|
||||||
ItemKey = interact.ItemKey,
|
ItemKey = interact.ItemKey,
|
||||||
QuantityGranted = interact.QuantityGranted,
|
QuantityGranted = interact.QuantityGranted,
|
||||||
|
CharacterGrantedQuantity = characterGrantedQuantity,
|
||||||
|
FloorGrantedQuantity = floorGrantedQuantity,
|
||||||
RemainingQuantity = interact.RemainingQuantity,
|
RemainingQuantity = interact.RemainingQuantity,
|
||||||
Consumed = interact.Consumed,
|
Consumed = interact.Consumed,
|
||||||
InventoryResponseJson = responseBody
|
InventoryResponseJson = responseBody
|
||||||
|
|||||||
@ -14,6 +14,10 @@ public class InteractLocationObjectResponse
|
|||||||
|
|
||||||
public int QuantityGranted { get; set; }
|
public int QuantityGranted { get; set; }
|
||||||
|
|
||||||
|
public int CharacterGrantedQuantity { get; set; }
|
||||||
|
|
||||||
|
public int FloorGrantedQuantity { get; set; }
|
||||||
|
|
||||||
public int RemainingQuantity { get; set; }
|
public int RemainingQuantity { get; set; }
|
||||||
|
|
||||||
public bool Consumed { get; set; }
|
public bool Consumed { get; set; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user