901 lines
25 KiB
GDScript
901 lines
25 KiB
GDScript
class_name PlayerInventoryLayout
|
|
extends Node
|
|
|
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
|
const ItemDataType = preload("res://items/item_data.gd")
|
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
|
const PlayerCoolerCapacityType = preload(
|
|
"res://progression/player_cooler_capacity.gd"
|
|
)
|
|
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|
|
|
const INVENTORY_COLUMNS: int = 9
|
|
const INVENTORY_CAPACITIES: Array[int] = [9, 18, 27, 36]
|
|
const BACKPACK_EXPANSION_COSTS: Array[int] = [1500, 4500, 9000]
|
|
const MAX_BACKPACK_LEVEL: int = 3
|
|
const MAX_INVENTORY_SLOT_COUNT: int = 36
|
|
const MAX_STORAGE_SLOT_COUNT: int = PlayerCoolerCapacityType.MAX_CAPACITY
|
|
const HOTBAR_SLOT_COUNT: int = 9
|
|
|
|
enum EntryKind {
|
|
ITEM,
|
|
CATCH,
|
|
}
|
|
|
|
enum InventoryContainer {
|
|
INVENTORY,
|
|
STORAGE,
|
|
HOTBAR,
|
|
}
|
|
|
|
signal layout_changed
|
|
signal backpack_capacity_changed(level: int, capacity: int)
|
|
|
|
var _bag: PlayerBagType
|
|
var _fish_inventory: FishInventoryType
|
|
var _item_catalog: ItemCatalogType
|
|
var _storage_capacity: PlayerCoolerCapacityType
|
|
var _placements: Dictionary[String, Dictionary] = {}
|
|
var _prepared_item_placements: Dictionary[StringName, Array] = {}
|
|
var _reconciling: bool = false
|
|
var _backpack_level: int = 0
|
|
|
|
|
|
func setup(
|
|
bag: PlayerBagType,
|
|
fish_inventory: FishInventoryType,
|
|
item_catalog: ItemCatalogType,
|
|
storage_capacity: PlayerCoolerCapacityType,
|
|
) -> void:
|
|
_bag = bag
|
|
_fish_inventory = fish_inventory
|
|
_item_catalog = item_catalog
|
|
_storage_capacity = storage_capacity
|
|
if _bag != null and not _bag.contents_changed.is_connected(_on_contents_changed):
|
|
_bag.contents_changed.connect(_on_contents_changed)
|
|
if (
|
|
_fish_inventory != null
|
|
and not _fish_inventory.catches_changed.is_connected(_on_contents_changed)
|
|
):
|
|
_fish_inventory.catches_changed.connect(_on_contents_changed)
|
|
if (
|
|
_storage_capacity != null
|
|
and not _storage_capacity.capacity_changed.is_connected(
|
|
_on_storage_capacity_changed
|
|
)
|
|
):
|
|
_storage_capacity.capacity_changed.connect(_on_storage_capacity_changed)
|
|
_reconcile(true)
|
|
|
|
|
|
func can_accept_item(item_id: StringName, quantity: int = 1) -> bool:
|
|
if not is_managed_item(item_id):
|
|
return true
|
|
if quantity <= 0:
|
|
return false
|
|
if _uses_individual_item_slots(item_id):
|
|
var available: int = _available_new_item_slots(
|
|
item_id, InventoryContainer.INVENTORY
|
|
)
|
|
for value: Variant in _prepared_item_placements.get(item_id, []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var placement := value as Dictionary
|
|
if (
|
|
int(placement.get("container", -1))
|
|
!= InventoryContainer.INVENTORY
|
|
and _prepared_item_placement_available(placement)
|
|
):
|
|
available += 1
|
|
return available >= quantity
|
|
var key := _find_entry_key(EntryKind.ITEM, item_id)
|
|
return (
|
|
not key.is_empty()
|
|
or _has_available_prepared_item_placement(item_id)
|
|
or _first_available_slot(InventoryContainer.INVENTORY) >= 0
|
|
)
|
|
|
|
|
|
func prepare_new_item_placement(
|
|
item_id: StringName,
|
|
target_container: InventoryContainer,
|
|
target_slot: int,
|
|
) -> bool:
|
|
if not is_managed_item(item_id):
|
|
return false
|
|
if (
|
|
(not _uses_individual_item_slots(item_id)
|
|
and not _find_entry_key(EntryKind.ITEM, item_id).is_empty())
|
|
or not _is_slot_valid(target_container, target_slot)
|
|
or not _key_at(target_container, target_slot).is_empty()
|
|
or _prepared_slot_is_occupied(target_container, target_slot)
|
|
):
|
|
return false
|
|
var prepared: Array = _prepared_item_placements.get(item_id, [])
|
|
prepared.append({
|
|
"container": int(target_container),
|
|
"slot": target_slot,
|
|
})
|
|
_prepared_item_placements[item_id] = prepared
|
|
return true
|
|
|
|
|
|
func cancel_prepared_item_placement(item_id: StringName) -> void:
|
|
_prepared_item_placements.erase(item_id)
|
|
|
|
|
|
func prepare_new_item_in_first_free(
|
|
item_id: StringName,
|
|
target_container: InventoryContainer,
|
|
) -> bool:
|
|
var slot := _first_available_slot(target_container)
|
|
return slot >= 0 and prepare_new_item_placement(
|
|
item_id, target_container, slot
|
|
)
|
|
|
|
|
|
func can_accept_item_in_storage(
|
|
item_id: StringName,
|
|
quantity: int = 1,
|
|
) -> bool:
|
|
if not is_managed_item(item_id):
|
|
return true
|
|
if quantity <= 0:
|
|
return false
|
|
if _uses_individual_item_slots(item_id):
|
|
return _available_new_item_slots(
|
|
item_id, InventoryContainer.STORAGE
|
|
) >= quantity
|
|
return (
|
|
not _find_entry_key(EntryKind.ITEM, item_id).is_empty()
|
|
or _first_available_slot(InventoryContainer.STORAGE) >= 0
|
|
)
|
|
|
|
|
|
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
|
if not catch_id.is_empty() and _placements.has(catch_key(catch_id)):
|
|
return true
|
|
return _first_free_slot(InventoryContainer.INVENTORY) >= 0
|
|
|
|
|
|
func is_managed_item(item_id: StringName) -> bool:
|
|
var item: ItemDataType = (
|
|
_item_catalog.get_item_by_id(item_id)
|
|
if _item_catalog != null else null
|
|
)
|
|
return item != null and not item.is_bait() and not item.is_lure()
|
|
|
|
|
|
func is_item_in_inventory(item_id: StringName) -> bool:
|
|
return _has_entry_in_container(
|
|
EntryKind.ITEM, item_id, InventoryContainer.INVENTORY
|
|
)
|
|
|
|
|
|
func is_catch_in_inventory(catch_id: StringName) -> bool:
|
|
return _has_entry_in_container(
|
|
EntryKind.CATCH, catch_id, InventoryContainer.INVENTORY
|
|
)
|
|
|
|
|
|
func is_item_carried(item_id: StringName) -> bool:
|
|
return _has_carried_entry(EntryKind.ITEM, item_id)
|
|
|
|
|
|
func is_catch_carried(catch_id: StringName) -> bool:
|
|
return _has_carried_entry(EntryKind.CATCH, catch_id)
|
|
|
|
|
|
func get_inventory_count() -> int:
|
|
return get_entries(InventoryContainer.INVENTORY).size()
|
|
|
|
|
|
func get_inventory_capacity() -> int:
|
|
return INVENTORY_CAPACITIES[_backpack_level]
|
|
|
|
|
|
func get_backpack_level() -> int:
|
|
return _backpack_level
|
|
|
|
|
|
func get_next_inventory_capacity() -> int:
|
|
if _backpack_level >= MAX_BACKPACK_LEVEL:
|
|
return -1
|
|
return INVENTORY_CAPACITIES[_backpack_level + 1]
|
|
|
|
|
|
func get_next_backpack_cost() -> int:
|
|
if _backpack_level >= MAX_BACKPACK_LEVEL:
|
|
return -1
|
|
return BACKPACK_EXPANSION_COSTS[_backpack_level]
|
|
|
|
|
|
func can_purchase_backpack(wallet: PlayerWalletType) -> bool:
|
|
var cost := get_next_backpack_cost()
|
|
return wallet != null and cost >= 0 and wallet.can_afford(cost)
|
|
|
|
|
|
func purchase_backpack(wallet: PlayerWalletType) -> bool:
|
|
if not can_purchase_backpack(wallet):
|
|
return false
|
|
var cost := get_next_backpack_cost()
|
|
if not wallet.debit(cost):
|
|
return false
|
|
_backpack_level += 1
|
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
|
layout_changed.emit()
|
|
return true
|
|
|
|
|
|
func restore_backpack_level(level: int) -> bool:
|
|
var validated := clampi(level, 0, MAX_BACKPACK_LEVEL)
|
|
var changed := _backpack_level != validated
|
|
_backpack_level = validated
|
|
if changed:
|
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
|
_reconcile(changed)
|
|
return true
|
|
|
|
|
|
func get_storage_count() -> int:
|
|
return get_entries(InventoryContainer.STORAGE).size()
|
|
|
|
|
|
func get_hotbar_count() -> int:
|
|
return get_entries(InventoryContainer.HOTBAR).size()
|
|
|
|
|
|
func get_storage_capacity() -> int:
|
|
return _storage_capacity.get_capacity() if _storage_capacity != null else 0
|
|
|
|
|
|
func get_entries(container: InventoryContainer) -> Array[Dictionary]:
|
|
var entries: Array[Dictionary] = []
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if int(placement.get("container", -1)) != int(container):
|
|
continue
|
|
var entry: Dictionary = placement.duplicate(true)
|
|
entry["key"] = key
|
|
entries.append(entry)
|
|
entries.sort_custom(
|
|
func(left: Dictionary, right: Dictionary) -> bool:
|
|
return int(left.get("slot", -1)) < int(right.get("slot", -1))
|
|
)
|
|
return entries
|
|
|
|
|
|
func get_entry(kind: EntryKind, identity: StringName) -> Dictionary:
|
|
var key := _find_entry_key(kind, identity)
|
|
if key.is_empty():
|
|
return {}
|
|
return _placements.get(key, {}).duplicate(true)
|
|
|
|
|
|
func get_container(kind: EntryKind, identity: StringName) -> int:
|
|
return int(
|
|
get_entry(kind, identity).get("container", -1)
|
|
)
|
|
|
|
|
|
func get_key_at(container: InventoryContainer, slot: int) -> String:
|
|
return _key_at(container, slot)
|
|
|
|
|
|
func get_entry_at(container: InventoryContainer, slot: int) -> Dictionary:
|
|
var key := _key_at(container, slot)
|
|
if key.is_empty():
|
|
return {}
|
|
var entry: Dictionary = _placements[key].duplicate(true)
|
|
entry["key"] = key
|
|
return entry
|
|
|
|
|
|
func move_entry(
|
|
kind: EntryKind,
|
|
identity: StringName,
|
|
target_container: InventoryContainer,
|
|
target_slot: int,
|
|
) -> bool:
|
|
var key := _find_entry_key_for_move(
|
|
kind, identity, target_container, target_slot
|
|
)
|
|
return move_entry_by_key(key, target_container, target_slot)
|
|
|
|
|
|
func move_entry_by_key(
|
|
key: String,
|
|
target_container: InventoryContainer,
|
|
target_slot: int,
|
|
) -> bool:
|
|
if not _placements.has(key) or not _is_slot_valid(target_container, target_slot):
|
|
return false
|
|
var source: Dictionary = _placements[key]
|
|
if (
|
|
int(source.get("container", -1)) == int(target_container)
|
|
and int(source.get("slot", -1)) == target_slot
|
|
):
|
|
return true
|
|
var displaced_key := _key_at(target_container, target_slot)
|
|
var source_container: int = int(source.get("container", InventoryContainer.INVENTORY))
|
|
var source_slot: int = int(source.get("slot", -1))
|
|
source["container"] = int(target_container)
|
|
source["slot"] = target_slot
|
|
_placements[key] = source
|
|
if not displaced_key.is_empty():
|
|
var displaced: Dictionary = _placements[displaced_key]
|
|
displaced["container"] = source_container
|
|
displaced["slot"] = source_slot
|
|
_placements[displaced_key] = displaced
|
|
layout_changed.emit()
|
|
return true
|
|
|
|
|
|
func move_entry_to_first_free(
|
|
kind: EntryKind,
|
|
identity: StringName,
|
|
target_container: InventoryContainer,
|
|
) -> bool:
|
|
var slot := _first_free_slot(target_container)
|
|
return slot >= 0 and move_entry(kind, identity, target_container, slot)
|
|
|
|
|
|
func move_entry_key_to_first_free(
|
|
key: String,
|
|
target_container: InventoryContainer,
|
|
) -> bool:
|
|
var slot := _first_free_slot(target_container)
|
|
return slot >= 0 and move_entry_by_key(key, target_container, slot)
|
|
|
|
|
|
func return_hotbar_entry_to_inventory(slot: int) -> bool:
|
|
var key := _key_at(InventoryContainer.HOTBAR, slot)
|
|
if key.is_empty():
|
|
return true
|
|
return move_entry_key_to_first_free(
|
|
key, InventoryContainer.INVENTORY
|
|
)
|
|
|
|
|
|
func return_all_hotbar_entries_to_inventory() -> bool:
|
|
var entries := get_entries(InventoryContainer.HOTBAR)
|
|
if get_inventory_count() + entries.size() > get_inventory_capacity():
|
|
return false
|
|
for entry: Dictionary in entries:
|
|
if not move_entry_key_to_first_free(
|
|
str(entry.get("key", "")), InventoryContainer.INVENTORY
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func to_save_data() -> Dictionary:
|
|
var serialized: Array[Dictionary] = []
|
|
var keys: Array[String] = []
|
|
keys.assign(_placements.keys())
|
|
keys.sort()
|
|
for key: String in keys:
|
|
var placement: Dictionary = _placements[key]
|
|
serialized.append({
|
|
"kind": int(placement.get("kind", EntryKind.ITEM)),
|
|
"identity": str(placement.get("identity", "")),
|
|
"unit": int(placement.get("unit", -1)),
|
|
"container": int(placement.get("container", InventoryContainer.INVENTORY)),
|
|
"slot": int(placement.get("slot", -1)),
|
|
})
|
|
return {
|
|
"backpack_level": _backpack_level,
|
|
"placements": serialized,
|
|
}
|
|
|
|
|
|
func restore_from_save_data(data: Dictionary) -> bool:
|
|
var level_value: Variant = data.get("backpack_level", 0)
|
|
if (
|
|
typeof(level_value) not in [TYPE_INT, TYPE_FLOAT]
|
|
or not is_equal_approx(float(level_value), floorf(float(level_value)))
|
|
):
|
|
return false
|
|
_backpack_level = clampi(int(level_value), 0, MAX_BACKPACK_LEVEL)
|
|
var restored: Dictionary[String, Dictionary] = {}
|
|
var restored_item_units: Dictionary[StringName, Dictionary] = {}
|
|
var values: Variant = data.get("placements", [])
|
|
if typeof(values) != TYPE_ARRAY:
|
|
return false
|
|
for value: Variant in values as Array:
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var record := value as Dictionary
|
|
var kind: int = int(record.get("kind", -1))
|
|
var identity := StringName(str(record.get("identity", "")))
|
|
var container: int = int(record.get("container", -1))
|
|
var slot: int = int(record.get("slot", -1))
|
|
if (
|
|
kind not in [EntryKind.ITEM, EntryKind.CATCH]
|
|
or identity.is_empty()
|
|
or container not in [
|
|
InventoryContainer.INVENTORY,
|
|
InventoryContainer.STORAGE,
|
|
InventoryContainer.HOTBAR,
|
|
]
|
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
|
):
|
|
continue
|
|
var unit: int = -1
|
|
var key: String
|
|
if (
|
|
kind == EntryKind.ITEM
|
|
and _uses_individual_item_slots(identity)
|
|
):
|
|
var used_units: Dictionary = restored_item_units.get(identity, {})
|
|
unit = int(record.get("unit", -1))
|
|
if unit < 0 or used_units.has(unit):
|
|
unit = _first_unused_unit(used_units)
|
|
used_units[unit] = true
|
|
restored_item_units[identity] = used_units
|
|
key = item_unit_key(identity, unit)
|
|
else:
|
|
key = _entry_key(kind as EntryKind, identity)
|
|
if restored.has(key) or _placement_slot_occupied(restored, container, slot):
|
|
continue
|
|
restored[key] = {
|
|
"kind": kind,
|
|
"identity": identity,
|
|
"unit": unit,
|
|
"container": container,
|
|
"slot": slot,
|
|
}
|
|
_prepared_item_placements.clear()
|
|
_placements = restored
|
|
_reconcile(true)
|
|
return true
|
|
|
|
|
|
func reset_to_defaults() -> void:
|
|
var capacity_changed := _backpack_level != 0
|
|
_backpack_level = 0
|
|
_prepared_item_placements.clear()
|
|
_placements.clear()
|
|
_reconcile(true)
|
|
if capacity_changed:
|
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
|
|
|
|
|
static func item_key(item_id: StringName) -> String:
|
|
return "item:%s" % String(item_id)
|
|
|
|
|
|
static func item_unit_key(item_id: StringName, unit: int) -> String:
|
|
return "%s:unit:%d" % [item_key(item_id), unit]
|
|
|
|
|
|
static func catch_key(catch_id: StringName) -> String:
|
|
return "catch:%s" % String(catch_id)
|
|
|
|
|
|
func _entry_key(kind: EntryKind, identity: StringName) -> String:
|
|
return item_key(identity) if kind == EntryKind.ITEM else catch_key(identity)
|
|
|
|
|
|
func _has_entry_in_container(
|
|
kind: EntryKind,
|
|
identity: StringName,
|
|
container: InventoryContainer,
|
|
) -> bool:
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if (
|
|
int(placement.get("kind", -1)) == int(kind)
|
|
and StringName(str(placement.get("identity", ""))) == identity
|
|
and int(placement.get("container", -1)) == int(container)
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _has_carried_entry(kind: EntryKind, identity: StringName) -> bool:
|
|
return (
|
|
_has_entry_in_container(kind, identity, InventoryContainer.INVENTORY)
|
|
or _has_entry_in_container(kind, identity, InventoryContainer.HOTBAR)
|
|
)
|
|
|
|
|
|
func _on_contents_changed() -> void:
|
|
_reconcile(false)
|
|
|
|
|
|
func _on_storage_capacity_changed(_level: int, _capacity: int) -> void:
|
|
_reconcile(true)
|
|
|
|
|
|
func _reconcile(force_signal: bool) -> void:
|
|
if _reconciling or _bag == null or _fish_inventory == null:
|
|
return
|
|
_reconciling = true
|
|
var expected: Dictionary[String, Dictionary] = {}
|
|
for owned in _bag.get_all_items():
|
|
if owned == null or not is_managed_item(owned.item_id):
|
|
continue
|
|
if _uses_individual_item_slots(owned.item_id):
|
|
_add_expected_individual_items(
|
|
expected, owned.item_id, owned.quantity
|
|
)
|
|
else:
|
|
var key := item_key(owned.item_id)
|
|
expected[key] = {
|
|
"kind": EntryKind.ITEM,
|
|
"identity": owned.item_id,
|
|
"unit": -1,
|
|
}
|
|
for fish_catch in _fish_inventory.get_all_catches():
|
|
if fish_catch == null or not fish_catch.is_valid():
|
|
continue
|
|
var key := catch_key(fish_catch.catch_id)
|
|
expected[key] = {"kind": EntryKind.CATCH, "identity": fish_catch.catch_id}
|
|
var changed: bool = false
|
|
for key: String in _placements.keys():
|
|
if expected.has(key):
|
|
continue
|
|
_placements.erase(key)
|
|
changed = true
|
|
_normalize_existing_placements()
|
|
for key: String in expected:
|
|
if _placements.has(key):
|
|
continue
|
|
var identity := StringName(str(expected[key].get("identity", "")))
|
|
var prepared: Dictionary = _take_prepared_item_placement(identity)
|
|
var target_container := InventoryContainer.INVENTORY
|
|
var target_slot: int = -1
|
|
if not prepared.is_empty():
|
|
target_container = int(
|
|
prepared["container"]
|
|
) as InventoryContainer
|
|
target_slot = int(prepared["slot"])
|
|
if target_slot < 0:
|
|
target_slot = _first_free_slot(target_container)
|
|
if target_slot < 0:
|
|
target_container = InventoryContainer.STORAGE
|
|
target_slot = _first_free_slot(target_container)
|
|
if target_slot < 0:
|
|
push_error("Inventory and storage cannot fit owned entry '%s'." % key)
|
|
continue
|
|
var placement: Dictionary = expected[key].duplicate(true)
|
|
placement["container"] = int(target_container)
|
|
placement["slot"] = target_slot
|
|
_placements[key] = placement
|
|
changed = true
|
|
_reconciling = false
|
|
if changed or force_signal:
|
|
layout_changed.emit()
|
|
|
|
|
|
func _add_expected_individual_items(
|
|
expected: Dictionary[String, Dictionary],
|
|
item_id: StringName,
|
|
quantity: int,
|
|
) -> void:
|
|
var existing_keys: Array[String] = _item_entry_keys(item_id)
|
|
existing_keys.sort_custom(
|
|
func(left: String, right: String) -> bool:
|
|
return _placement_preservation_priority(left) < (
|
|
_placement_preservation_priority(right)
|
|
)
|
|
)
|
|
var used_units: Dictionary[int, bool] = {}
|
|
var retained: int = 0
|
|
for key: String in existing_keys:
|
|
if retained >= quantity:
|
|
break
|
|
var placement: Dictionary = _placements[key]
|
|
var unit: int = int(placement.get("unit", -1))
|
|
if unit < 0 or used_units.has(unit):
|
|
continue
|
|
used_units[unit] = true
|
|
expected[key] = {
|
|
"kind": EntryKind.ITEM,
|
|
"identity": item_id,
|
|
"unit": unit,
|
|
}
|
|
retained += 1
|
|
while retained < quantity:
|
|
var unit := _first_unused_unit(used_units)
|
|
used_units[unit] = true
|
|
var key := item_unit_key(item_id, unit)
|
|
expected[key] = {
|
|
"kind": EntryKind.ITEM,
|
|
"identity": item_id,
|
|
"unit": unit,
|
|
}
|
|
retained += 1
|
|
|
|
|
|
func _placement_preservation_priority(key: String) -> int:
|
|
var placement: Dictionary = _placements.get(key, {})
|
|
var container: int = int(placement.get("container", -1))
|
|
match container:
|
|
InventoryContainer.HOTBAR:
|
|
return 0
|
|
InventoryContainer.INVENTORY:
|
|
return 1
|
|
InventoryContainer.STORAGE:
|
|
return 2
|
|
return 3
|
|
|
|
|
|
func _normalize_existing_placements() -> void:
|
|
var occupied_inventory: Dictionary[int, bool] = {}
|
|
var occupied_storage: Dictionary[int, bool] = {}
|
|
var occupied_hotbar: Dictionary[int, bool] = {}
|
|
var invalid_keys: Array[String] = []
|
|
var keys: Array[String] = []
|
|
keys.assign(_placements.keys())
|
|
keys.sort()
|
|
for key: String in keys:
|
|
var placement: Dictionary = _placements[key]
|
|
var container: int = int(placement.get("container", -1))
|
|
var slot: int = int(placement.get("slot", -1))
|
|
var occupied: Dictionary[int, bool]
|
|
match container:
|
|
InventoryContainer.INVENTORY:
|
|
occupied = occupied_inventory
|
|
InventoryContainer.STORAGE:
|
|
occupied = occupied_storage
|
|
InventoryContainer.HOTBAR:
|
|
occupied = occupied_hotbar
|
|
_:
|
|
occupied = {}
|
|
if (
|
|
container not in [
|
|
InventoryContainer.INVENTORY,
|
|
InventoryContainer.STORAGE,
|
|
InventoryContainer.HOTBAR,
|
|
]
|
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
|
or occupied.has(slot)
|
|
):
|
|
invalid_keys.append(key)
|
|
continue
|
|
occupied[slot] = true
|
|
for key: String in invalid_keys:
|
|
# Invalid records must stop occupying their old slot before free-space
|
|
# lookup. Otherwise a duplicate can incorrectly reserve a valid slot.
|
|
var placement: Dictionary = _placements[key]
|
|
_placements.erase(key)
|
|
var target := InventoryContainer.INVENTORY
|
|
var slot := _first_free_slot(target)
|
|
if slot < 0:
|
|
target = InventoryContainer.STORAGE
|
|
slot = _first_free_slot(target)
|
|
if slot < 0:
|
|
_placements.erase(key)
|
|
continue
|
|
placement["container"] = int(target)
|
|
placement["slot"] = slot
|
|
_placements[key] = placement
|
|
|
|
|
|
func _first_free_slot(container: InventoryContainer) -> int:
|
|
var capacity := (
|
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
|
else get_storage_capacity()
|
|
)
|
|
for slot: int in capacity:
|
|
if (
|
|
_key_at(container, slot).is_empty()
|
|
and not _prepared_slot_is_occupied(container, slot)
|
|
):
|
|
return slot
|
|
return -1
|
|
|
|
|
|
func _first_available_slot(container: InventoryContainer) -> int:
|
|
var capacity := (
|
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
|
else get_storage_capacity()
|
|
)
|
|
for slot: int in capacity:
|
|
if (
|
|
_key_at(container, slot).is_empty()
|
|
and not _prepared_slot_is_occupied(container, slot)
|
|
):
|
|
return slot
|
|
return -1
|
|
|
|
|
|
func _has_available_prepared_item_placement(item_id: StringName) -> bool:
|
|
for value: Variant in _prepared_item_placements.get(item_id, []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
if _prepared_item_placement_available(value as Dictionary):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _take_prepared_item_placement(item_id: StringName) -> Dictionary:
|
|
var prepared: Array = _prepared_item_placements.get(item_id, [])
|
|
while not prepared.is_empty():
|
|
var value: Variant = prepared.pop_front()
|
|
if prepared.is_empty():
|
|
_prepared_item_placements.erase(item_id)
|
|
else:
|
|
_prepared_item_placements[item_id] = prepared
|
|
if (
|
|
typeof(value) == TYPE_DICTIONARY
|
|
and _prepared_item_placement_available(value as Dictionary)
|
|
):
|
|
return (value as Dictionary).duplicate(true)
|
|
return {}
|
|
|
|
|
|
func _prepared_item_placement_available(placement: Dictionary) -> bool:
|
|
var container: int = int(placement.get("container", -1))
|
|
var slot: int = int(placement.get("slot", -1))
|
|
if (
|
|
container not in [
|
|
InventoryContainer.INVENTORY,
|
|
InventoryContainer.STORAGE,
|
|
InventoryContainer.HOTBAR,
|
|
]
|
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
|
):
|
|
return false
|
|
return _key_at(container as InventoryContainer, slot).is_empty()
|
|
|
|
|
|
func _prepared_slot_is_occupied(
|
|
container: InventoryContainer,
|
|
slot: int,
|
|
) -> bool:
|
|
for prepared: Array in _prepared_item_placements.values():
|
|
for value: Variant in prepared:
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var placement := value as Dictionary
|
|
if (
|
|
int(placement.get("container", -1)) == int(container)
|
|
and int(placement.get("slot", -1)) == slot
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _available_new_item_slots(
|
|
item_id: StringName,
|
|
container: InventoryContainer,
|
|
) -> int:
|
|
var available: int = 0
|
|
for value: Variant in _prepared_item_placements.get(item_id, []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var placement := value as Dictionary
|
|
if (
|
|
int(placement.get("container", -1)) == int(container)
|
|
and _prepared_item_placement_available(placement)
|
|
):
|
|
available += 1
|
|
var capacity := (
|
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
|
else get_storage_capacity()
|
|
)
|
|
for slot: int in capacity:
|
|
if (
|
|
_key_at(container, slot).is_empty()
|
|
and not _prepared_slot_is_occupied(container, slot)
|
|
):
|
|
available += 1
|
|
return available
|
|
|
|
|
|
func _uses_individual_item_slots(item_id: StringName) -> bool:
|
|
return DecorCatalogType.has_product(item_id)
|
|
|
|
|
|
func _item_entry_keys(item_id: StringName) -> Array[String]:
|
|
var result: Array[String] = []
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if (
|
|
int(placement.get("kind", -1)) == EntryKind.ITEM
|
|
and StringName(str(placement.get("identity", ""))) == item_id
|
|
):
|
|
result.append(key)
|
|
return result
|
|
|
|
|
|
func _find_entry_key(kind: EntryKind, identity: StringName) -> String:
|
|
var preferred_key: String = ""
|
|
var preferred_priority: int = 99
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if (
|
|
int(placement.get("kind", -1)) != int(kind)
|
|
or StringName(str(placement.get("identity", ""))) != identity
|
|
):
|
|
continue
|
|
var priority := _placement_preservation_priority(key)
|
|
if priority < preferred_priority:
|
|
preferred_key = key
|
|
preferred_priority = priority
|
|
return preferred_key
|
|
|
|
|
|
func _find_entry_key_for_move(
|
|
kind: EntryKind,
|
|
identity: StringName,
|
|
target_container: InventoryContainer,
|
|
target_slot: int,
|
|
) -> String:
|
|
var target_key := _key_at(target_container, target_slot)
|
|
if not target_key.is_empty():
|
|
var target: Dictionary = _placements[target_key]
|
|
if (
|
|
int(target.get("kind", -1)) == int(kind)
|
|
and StringName(str(target.get("identity", ""))) == identity
|
|
):
|
|
return target_key
|
|
var preferred_key: String = ""
|
|
var preferred_priority: int = 99
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if (
|
|
int(placement.get("kind", -1)) != int(kind)
|
|
or StringName(str(placement.get("identity", ""))) != identity
|
|
):
|
|
continue
|
|
var container: int = int(placement.get("container", -1))
|
|
var priority: int = (
|
|
0 if container == InventoryContainer.INVENTORY
|
|
else 1 if container == InventoryContainer.STORAGE
|
|
else 2
|
|
)
|
|
if priority < preferred_priority:
|
|
preferred_key = key
|
|
preferred_priority = priority
|
|
return preferred_key
|
|
|
|
|
|
func _first_unused_unit(used_units: Dictionary) -> int:
|
|
var unit: int = 0
|
|
while used_units.has(unit):
|
|
unit += 1
|
|
return unit
|
|
|
|
|
|
func _key_at(container: InventoryContainer, slot: int) -> String:
|
|
for key: String in _placements:
|
|
var placement: Dictionary = _placements[key]
|
|
if (
|
|
int(placement.get("container", -1)) == int(container)
|
|
and int(placement.get("slot", -1)) == slot
|
|
):
|
|
return key
|
|
return ""
|
|
|
|
|
|
func _is_slot_valid(container: InventoryContainer, slot: int) -> bool:
|
|
if slot < 0:
|
|
return false
|
|
return slot < (
|
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
|
else get_storage_capacity()
|
|
)
|
|
|
|
|
|
func _placement_slot_occupied(
|
|
placements: Dictionary[String, Dictionary],
|
|
container: int,
|
|
slot: int,
|
|
) -> bool:
|
|
for placement: Dictionary in placements.values():
|
|
if (
|
|
int(placement.get("container", -1)) == container
|
|
and int(placement.get("slot", -1)) == slot
|
|
):
|
|
return true
|
|
return false
|