Make decor occupy individual inventory slots

This commit is contained in:
Alexander Sellite 2026-09-01 17:29:57 -04:00
parent f2239d248a
commit 4196cda1d5
9 changed files with 516 additions and 79 deletions

View file

@ -4,6 +4,7 @@ extends Node
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const DEFAULT_UNLOCKED_BAIT_IDS: Array[StringName] = [&"worms"]
const MIN_STORAGE_SLOT_COUNT: int = 15
@ -62,9 +63,35 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
func add_item_to_storage(item_id: StringName, quantity: int = 1) -> bool:
var existing: OwnedItemType = get_owned_item(item_id)
var item: ItemDataType = _resolve_available_item(item_id)
if item != null and DecorCatalogType.has_product(item_id):
if (
not _can_own_quantity(item, existing, quantity)
or _inventory_layout == null
or not _inventory_layout.can_accept_item_in_storage(
item_id, quantity
)
):
return false
for _unit: int in quantity:
if not _inventory_layout.prepare_new_item_in_first_free(
item_id,
PlayerInventoryLayout.InventoryContainer.STORAGE,
):
_inventory_layout.cancel_prepared_item_placement(item_id)
return false
if existing != null:
existing.quantity += quantity
else:
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
owned.storage_slot = _first_available_storage_slot(item, _items)
_items.append(owned)
contents_changed.emit()
return true
if existing != null:
return add_item(item_id, quantity)
var item: ItemDataType = _resolve_available_item(item_id)
if (
item == null
or quantity <= 0
@ -87,6 +114,14 @@ func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
if item == null or quantity <= 0:
return false
var existing: OwnedItemType = get_owned_item(item_id)
if DecorCatalogType.has_product(item_id):
return (
_can_own_quantity(item, existing, quantity)
and (
_inventory_layout == null
or _inventory_layout.can_accept_item(item_id, quantity)
)
)
if existing != null:
return (
item.stackable
@ -226,7 +261,7 @@ func replace_all_items(items: Array[OwnedItemType]) -> bool:
var item: ItemDataType = _resolve_valid_item(owned.item_id)
if item == null:
return false
var maximum: int = item.max_stack if item.stackable else 1
var maximum: int = _maximum_owned_quantity(item)
if owned.quantity > maximum:
return false
seen[owned.item_id] = true
@ -319,3 +354,22 @@ func _resolve_valid_item(item_id: StringName) -> ItemDataType:
func _resolve_available_item(item_id: StringName) -> ItemDataType:
var item: ItemDataType = _resolve_valid_item(item_id)
return item if item != null and item.active else null
func _can_own_quantity(
item: ItemDataType,
existing: OwnedItemType,
quantity: int,
) -> bool:
if item == null or quantity <= 0:
return false
var current: int = existing.quantity if existing != null else 0
return current + quantity <= _maximum_owned_quantity(item)
func _maximum_owned_quantity(item: ItemDataType) -> int:
if item == null:
return 0
if DecorCatalogType.has_product(item.item_id):
return mini(item.max_stack, DecorCatalogType.MAX_OWNED_PER_PRODUCT)
return item.max_stack if item.stackable else 1

View file

@ -9,6 +9,7 @@ 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]
@ -37,7 +38,7 @@ var _fish_inventory: FishInventoryType
var _item_catalog: ItemCatalogType
var _storage_capacity: PlayerCoolerCapacityType
var _placements: Dictionary[String, Dictionary] = {}
var _prepared_item_placements: Dictionary[String, Dictionary] = {}
var _prepared_item_placements: Dictionary[StringName, Array] = {}
var _reconciling: bool = false
var _backpack_level: int = 0
@ -69,14 +70,31 @@ func setup(
_reconcile(true)
func can_accept_item(item_id: StringName) -> bool:
func can_accept_item(item_id: StringName, quantity: int = 1) -> bool:
if not is_managed_item(item_id):
return true
var key := item_key(item_id)
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 (
_placements.has(key)
or _is_prepared_item_placement_available(key)
or _first_free_slot(InventoryContainer.INVENTORY) >= 0
not key.is_empty()
or _has_available_prepared_item_placement(item_id)
or _first_available_slot(InventoryContainer.INVENTORY) >= 0
)
@ -87,30 +105,53 @@ func prepare_new_item_placement(
) -> bool:
if not is_managed_item(item_id):
return false
var key := item_key(item_id)
if (
_placements.has(key)
(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
_prepared_item_placements[key] = {
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_key(item_id))
_prepared_item_placements.erase(item_id)
func can_accept_item_in_storage(item_id: StringName) -> bool:
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
var key := item_key(item_id)
return _placements.has(key) or _first_free_slot(InventoryContainer.STORAGE) >= 0
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:
@ -128,19 +169,23 @@ func is_managed_item(item_id: StringName) -> bool:
func is_item_in_inventory(item_id: StringName) -> bool:
return _is_entry_in_container(item_key(item_id), InventoryContainer.INVENTORY)
return _has_entry_in_container(
EntryKind.ITEM, item_id, InventoryContainer.INVENTORY
)
func is_catch_in_inventory(catch_id: StringName) -> bool:
return _is_entry_in_container(catch_key(catch_id), InventoryContainer.INVENTORY)
return _has_entry_in_container(
EntryKind.CATCH, catch_id, InventoryContainer.INVENTORY
)
func is_item_carried(item_id: StringName) -> bool:
return _is_entry_carried(item_key(item_id))
return _has_carried_entry(EntryKind.ITEM, item_id)
func is_catch_carried(catch_id: StringName) -> bool:
return _is_entry_carried(catch_key(catch_id))
return _has_carried_entry(EntryKind.CATCH, catch_id)
func get_inventory_count() -> int:
@ -223,7 +268,9 @@ func get_entries(container: InventoryContainer) -> Array[Dictionary]:
func get_entry(kind: EntryKind, identity: StringName) -> Dictionary:
var key := _entry_key(kind, identity)
var key := _find_entry_key(kind, identity)
if key.is_empty():
return {}
return _placements.get(key, {}).duplicate(true)
@ -252,7 +299,17 @@ func move_entry(
target_container: InventoryContainer,
target_slot: int,
) -> bool:
var key := _entry_key(kind, identity)
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]
@ -285,15 +342,20 @@ func move_entry_to_first_free(
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
var placement: Dictionary = _placements[key]
return move_entry_to_first_free(
int(placement.get("kind", -1)) as EntryKind,
StringName(str(placement.get("identity", ""))),
InventoryContainer.INVENTORY,
return move_entry_key_to_first_free(
key, InventoryContainer.INVENTORY
)
@ -302,10 +364,8 @@ func return_all_hotbar_entries_to_inventory() -> bool:
if get_inventory_count() + entries.size() > get_inventory_capacity():
return false
for entry: Dictionary in entries:
if not move_entry_to_first_free(
int(entry.get("kind", -1)) as EntryKind,
StringName(str(entry.get("identity", ""))),
InventoryContainer.INVENTORY,
if not move_entry_key_to_first_free(
str(entry.get("key", "")), InventoryContainer.INVENTORY
):
return false
return true
@ -321,6 +381,7 @@ func to_save_data() -> Dictionary:
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)),
})
@ -339,6 +400,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
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
@ -361,12 +423,27 @@ func restore_from_save_data(data: Dictionary) -> bool:
or not _is_slot_valid(container as InventoryContainer, slot)
):
continue
var key := _entry_key(kind as EntryKind, identity)
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,
}
@ -390,6 +467,10 @@ 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)
@ -398,15 +479,27 @@ func _entry_key(kind: EntryKind, identity: StringName) -> String:
return item_key(identity) if kind == EntryKind.ITEM else catch_key(identity)
func _is_entry_in_container(key: String, container: InventoryContainer) -> bool:
var placement: Dictionary = _placements.get(key, {})
return int(placement.get("container", -1)) == int(container)
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 _is_entry_carried(key: String) -> bool:
var placement: Dictionary = _placements.get(key, {})
var container: int = int(placement.get("container", -1))
return container in [InventoryContainer.INVENTORY, InventoryContainer.HOTBAR]
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:
@ -425,8 +518,17 @@ func _reconcile(force_signal: bool) -> void:
for owned in _bag.get_all_items():
if owned == null or not is_managed_item(owned.item_id):
continue
var key := item_key(owned.item_id)
expected[key] = {"kind": EntryKind.ITEM, "identity": owned.item_id}
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
@ -442,15 +544,15 @@ func _reconcile(force_signal: bool) -> void:
for key: String in expected:
if _placements.has(key):
continue
var prepared: Dictionary = _prepared_item_placements.get(key, {})
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 _is_prepared_item_placement_available(key):
if not prepared.is_empty():
target_container = int(
prepared["container"]
) as InventoryContainer
target_slot = int(prepared["slot"])
_prepared_item_placements.erase(key)
if target_slot < 0:
target_slot = _first_free_slot(target_container)
if target_slot < 0:
@ -469,6 +571,59 @@ func _reconcile(force_signal: bool) -> void:
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] = {}
@ -528,15 +683,55 @@ func _first_free_slot(container: InventoryContainer) -> int:
else get_storage_capacity()
)
for slot: int in capacity:
if _key_at(container, slot).is_empty():
if (
_key_at(container, slot).is_empty()
and not _prepared_slot_is_occupied(container, slot)
):
return slot
return -1
func _is_prepared_item_placement_available(key: String) -> bool:
var placement: Dictionary = _prepared_item_placements.get(key, {})
if placement.is_empty():
return false
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 (
@ -555,13 +750,120 @@ func _prepared_slot_is_occupied(
container: InventoryContainer,
slot: int,
) -> bool:
for placement: Dictionary in _prepared_item_placements.values():
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 int(placement.get("slot", -1)) == slot
and _prepared_item_placement_available(placement)
):
return true
return false
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: