forked from woofmeow/straywild
Add gathering, unified inventory, and real-time world
This commit is contained in:
parent
173371e6fc
commit
fa57edca83
113 changed files with 6700 additions and 1307 deletions
|
|
@ -9,6 +9,7 @@ signal catches_changed
|
|||
var _catches: Array[FishCatchType] = []
|
||||
var _next_catch_sequence: int = 1
|
||||
var _reservation_service: PlayerAssetReservationService
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
|
||||
|
||||
func set_reservation_service(
|
||||
|
|
@ -17,11 +18,24 @@ func set_reservation_service(
|
|||
_reservation_service = reservation_service
|
||||
|
||||
|
||||
func add_catch(fish_catch: FishCatchType) -> void:
|
||||
func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
|
||||
_inventory_layout = layout
|
||||
|
||||
|
||||
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
||||
return (
|
||||
_inventory_layout == null
|
||||
or _inventory_layout.can_accept_catch(catch_id)
|
||||
)
|
||||
|
||||
|
||||
func add_catch(fish_catch: FishCatchType) -> bool:
|
||||
if fish_catch == null or not fish_catch.is_valid():
|
||||
return
|
||||
return false
|
||||
if get_catch_by_id(fish_catch.catch_id) != null:
|
||||
return
|
||||
return false
|
||||
if not can_accept_catch(fish_catch.catch_id):
|
||||
return false
|
||||
if fish_catch.catch_sequence <= 0:
|
||||
fish_catch.catch_sequence = _next_catch_sequence
|
||||
_next_catch_sequence = maxi(
|
||||
|
|
@ -34,6 +48,7 @@ func add_catch(fish_catch: FishCatchType) -> void:
|
|||
get_count(fish_catch.fish_id)
|
||||
)
|
||||
catches_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func get_count(fish_id: StringName) -> int:
|
||||
|
|
|
|||
|
|
@ -13,11 +13,14 @@ enum StorageGroup {
|
|||
NONE,
|
||||
EQUIPMENT,
|
||||
ITEMS,
|
||||
BAIT,
|
||||
LURES,
|
||||
}
|
||||
|
||||
signal contents_changed
|
||||
|
||||
var _catalog: ItemCatalogType
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
var _items: Array[OwnedItemType] = []
|
||||
var _unlocked_bait_ids: Array[StringName] = (
|
||||
DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
|
||||
|
|
@ -28,11 +31,21 @@ func setup(catalog: ItemCatalogType) -> void:
|
|||
_catalog = catalog
|
||||
|
||||
|
||||
func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
|
||||
_inventory_layout = layout
|
||||
|
||||
|
||||
func add_item(item_id: StringName, quantity: int = 1) -> bool:
|
||||
if not can_add_item(item_id, quantity):
|
||||
return false
|
||||
var item: ItemDataType = _resolve_valid_item(item_id)
|
||||
var existing: OwnedItemType = get_owned_item(item_id)
|
||||
if (
|
||||
existing == null
|
||||
and _inventory_layout != null
|
||||
and not _inventory_layout.can_accept_item(item_id)
|
||||
):
|
||||
return false
|
||||
if existing != null:
|
||||
existing.quantity += quantity
|
||||
else:
|
||||
|
|
@ -57,6 +70,11 @@ func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
|
|||
item.stackable
|
||||
and existing.quantity + quantity <= item.max_stack
|
||||
)
|
||||
if (
|
||||
_inventory_layout != null
|
||||
and not _inventory_layout.can_accept_item(item_id)
|
||||
):
|
||||
return false
|
||||
return quantity <= (item.max_stack if item.stackable else 1)
|
||||
|
||||
|
||||
|
|
@ -252,8 +270,12 @@ func _first_available_storage_slot(
|
|||
|
||||
|
||||
func _storage_group_for_item(item: ItemDataType) -> StorageGroup:
|
||||
if item == null or item.is_bait() or item.is_lure():
|
||||
if item == null:
|
||||
return StorageGroup.NONE
|
||||
if item.is_bait():
|
||||
return StorageGroup.BAIT
|
||||
if item.is_lure():
|
||||
return StorageGroup.LURES
|
||||
return (
|
||||
StorageGroup.ITEMS
|
||||
if item.category == ItemDataType.Category.CONSUMABLE
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ signal selected_assignment_changed(
|
|||
var _bag: PlayerBagType
|
||||
var _catalog: ItemCatalogType
|
||||
var _fish_inventory: FishInventoryType
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
var _slots: Array[StringName] = []
|
||||
var _fish_slots: Array[StringName] = []
|
||||
var _selected_slot: int = 0
|
||||
|
|
@ -40,10 +41,12 @@ func setup(
|
|||
bag: PlayerBagType,
|
||||
catalog: ItemCatalogType,
|
||||
fish_inventory: FishInventoryType = null,
|
||||
inventory_layout: PlayerInventoryLayout = null,
|
||||
) -> void:
|
||||
_bag = bag
|
||||
_catalog = catalog
|
||||
_fish_inventory = fish_inventory
|
||||
_inventory_layout = inventory_layout
|
||||
if _bag != null and not _bag.contents_changed.is_connected(
|
||||
_on_bag_contents_changed
|
||||
):
|
||||
|
|
@ -55,6 +58,11 @@ func setup(
|
|||
)
|
||||
):
|
||||
_fish_inventory.catches_changed.connect(_on_fish_inventory_changed)
|
||||
if (
|
||||
_inventory_layout != null
|
||||
and not _inventory_layout.layout_changed.is_connected(_validate_assignments)
|
||||
):
|
||||
_inventory_layout.layout_changed.connect(_validate_assignments)
|
||||
_validate_assignments()
|
||||
|
||||
|
||||
|
|
@ -63,13 +71,26 @@ func assign_item(slot_index: int, item_id: StringName) -> bool:
|
|||
return false
|
||||
if _slots[slot_index] == item_id:
|
||||
return true
|
||||
var source_index: int = _slots.find(item_id)
|
||||
var displaced_item: StringName = _slots[slot_index]
|
||||
var displaced_fish: StringName = _fish_slots[slot_index]
|
||||
if (
|
||||
_inventory_layout != null
|
||||
and not _inventory_layout.move_entry(
|
||||
PlayerInventoryLayout.EntryKind.ITEM,
|
||||
item_id,
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
slot_index,
|
||||
)
|
||||
):
|
||||
return false
|
||||
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||
for index: int in range(SLOT_COUNT):
|
||||
if index != slot_index and _slots[index] == item_id:
|
||||
_slots[index] = StringName()
|
||||
selected_slot_was_affected = (
|
||||
selected_slot_was_affected or index == _selected_slot
|
||||
)
|
||||
if source_index >= 0 and source_index != slot_index:
|
||||
_slots[source_index] = displaced_item
|
||||
_fish_slots[source_index] = displaced_fish
|
||||
selected_slot_was_affected = (
|
||||
selected_slot_was_affected or source_index == _selected_slot
|
||||
)
|
||||
_slots[slot_index] = item_id
|
||||
_fish_slots[slot_index] = StringName()
|
||||
slots_changed.emit()
|
||||
|
|
@ -83,13 +104,26 @@ func assign_fish(slot_index: int, catch_id: StringName) -> bool:
|
|||
return false
|
||||
if _fish_slots[slot_index] == catch_id:
|
||||
return true
|
||||
var source_index: int = _fish_slots.find(catch_id)
|
||||
var displaced_item: StringName = _slots[slot_index]
|
||||
var displaced_fish: StringName = _fish_slots[slot_index]
|
||||
if (
|
||||
_inventory_layout != null
|
||||
and not _inventory_layout.move_entry(
|
||||
PlayerInventoryLayout.EntryKind.CATCH,
|
||||
catch_id,
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
slot_index,
|
||||
)
|
||||
):
|
||||
return false
|
||||
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||
for index: int in range(SLOT_COUNT):
|
||||
if index != slot_index and _fish_slots[index] == catch_id:
|
||||
_fish_slots[index] = StringName()
|
||||
selected_slot_was_affected = (
|
||||
selected_slot_was_affected or index == _selected_slot
|
||||
)
|
||||
if source_index >= 0 and source_index != slot_index:
|
||||
_slots[source_index] = displaced_item
|
||||
_fish_slots[source_index] = displaced_fish
|
||||
selected_slot_was_affected = (
|
||||
selected_slot_was_affected or source_index == _selected_slot
|
||||
)
|
||||
_slots[slot_index] = StringName()
|
||||
_fish_slots[slot_index] = catch_id
|
||||
slots_changed.emit()
|
||||
|
|
@ -107,6 +141,11 @@ func clear_slot(slot_index: int) -> bool:
|
|||
)
|
||||
):
|
||||
return false
|
||||
if (
|
||||
_inventory_layout != null
|
||||
and not _inventory_layout.return_hotbar_entry_to_inventory(slot_index)
|
||||
):
|
||||
return false
|
||||
_slots[slot_index] = StringName()
|
||||
_fish_slots[slot_index] = StringName()
|
||||
slots_changed.emit()
|
||||
|
|
@ -122,6 +161,39 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
|||
or from_index == to_index
|
||||
):
|
||||
return false
|
||||
if _inventory_layout != null:
|
||||
var kind: PlayerInventoryLayout.EntryKind
|
||||
var identity: StringName
|
||||
if not _fish_slots[from_index].is_empty():
|
||||
kind = PlayerInventoryLayout.EntryKind.CATCH
|
||||
identity = _fish_slots[from_index]
|
||||
elif not _slots[from_index].is_empty():
|
||||
kind = PlayerInventoryLayout.EntryKind.ITEM
|
||||
identity = _slots[from_index]
|
||||
else:
|
||||
kind = (
|
||||
PlayerInventoryLayout.EntryKind.CATCH
|
||||
if not _fish_slots[to_index].is_empty()
|
||||
else PlayerInventoryLayout.EntryKind.ITEM
|
||||
)
|
||||
identity = (
|
||||
_fish_slots[to_index]
|
||||
if not _fish_slots[to_index].is_empty()
|
||||
else _slots[to_index]
|
||||
)
|
||||
var temporary_index: int = from_index
|
||||
from_index = to_index
|
||||
to_index = temporary_index
|
||||
if (
|
||||
identity.is_empty()
|
||||
or not _inventory_layout.move_entry(
|
||||
kind,
|
||||
identity,
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
to_index,
|
||||
)
|
||||
):
|
||||
return false
|
||||
var temporary: StringName = _slots[from_index]
|
||||
_slots[from_index] = _slots[to_index]
|
||||
_slots[to_index] = temporary
|
||||
|
|
@ -134,6 +206,50 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func move_slot_to_container(
|
||||
slot_index: int,
|
||||
target_container: int,
|
||||
target_slot: int,
|
||||
) -> bool:
|
||||
if not _is_slot_valid(slot_index) or _inventory_layout == null:
|
||||
return false
|
||||
var kind: PlayerInventoryLayout.EntryKind
|
||||
var identity: StringName
|
||||
if not _fish_slots[slot_index].is_empty():
|
||||
kind = PlayerInventoryLayout.EntryKind.CATCH
|
||||
identity = _fish_slots[slot_index]
|
||||
elif not _slots[slot_index].is_empty():
|
||||
kind = PlayerInventoryLayout.EntryKind.ITEM
|
||||
identity = _slots[slot_index]
|
||||
else:
|
||||
return false
|
||||
if not _inventory_layout.move_entry(
|
||||
kind,
|
||||
identity,
|
||||
target_container,
|
||||
target_slot,
|
||||
):
|
||||
return false
|
||||
_slots[slot_index] = StringName()
|
||||
_fish_slots[slot_index] = StringName()
|
||||
var replacement := _inventory_layout.get_entry_at(
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
slot_index,
|
||||
)
|
||||
if not replacement.is_empty():
|
||||
var replacement_identity := StringName(
|
||||
str(replacement.get("identity", ""))
|
||||
)
|
||||
if int(replacement.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||
_fish_slots[slot_index] = replacement_identity
|
||||
else:
|
||||
_slots[slot_index] = replacement_identity
|
||||
slots_changed.emit()
|
||||
if slot_index == _selected_slot:
|
||||
_emit_selected_assignment()
|
||||
return true
|
||||
|
||||
|
||||
func select_slot(slot_index: int) -> bool:
|
||||
if not _is_slot_valid(slot_index):
|
||||
return false
|
||||
|
|
@ -202,7 +318,13 @@ func replace_state(
|
|||
slots: Array[StringName],
|
||||
selected_slot: int,
|
||||
fish_slots: Array[StringName] = [],
|
||||
synchronize_layout: bool = true,
|
||||
) -> bool:
|
||||
if (
|
||||
synchronize_layout and _inventory_layout != null
|
||||
and not _inventory_layout.return_all_hotbar_entries_to_inventory()
|
||||
):
|
||||
return false
|
||||
var normalized: Array[StringName] = []
|
||||
normalized.resize(SLOT_COUNT)
|
||||
normalized.fill(StringName())
|
||||
|
|
@ -227,6 +349,22 @@ func replace_state(
|
|||
seen_fish[catch_id] = true
|
||||
_slots = normalized
|
||||
_fish_slots = normalized_fish
|
||||
if _inventory_layout != null and synchronize_layout:
|
||||
for index: int in SLOT_COUNT:
|
||||
if not _slots[index].is_empty():
|
||||
_inventory_layout.move_entry(
|
||||
PlayerInventoryLayout.EntryKind.ITEM,
|
||||
_slots[index],
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
index,
|
||||
)
|
||||
elif not _fish_slots[index].is_empty():
|
||||
_inventory_layout.move_entry(
|
||||
PlayerInventoryLayout.EntryKind.CATCH,
|
||||
_fish_slots[index],
|
||||
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||
index,
|
||||
)
|
||||
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
||||
slots_changed.emit()
|
||||
_emit_selected_assignment()
|
||||
|
|
@ -249,7 +387,17 @@ func _can_assign(item_id: StringName) -> bool:
|
|||
if _catalog == null:
|
||||
return false
|
||||
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
||||
return item != null and item.is_available() and item.hotbar_allowed
|
||||
return (
|
||||
item != null
|
||||
and item.is_available()
|
||||
and item.hotbar_allowed
|
||||
and (
|
||||
_inventory_layout == null
|
||||
or _inventory_layout.get_container(
|
||||
PlayerInventoryLayout.EntryKind.ITEM, item_id
|
||||
) >= 0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _can_assign_fish(catch_id: StringName) -> bool:
|
||||
|
|
@ -257,6 +405,12 @@ func _can_assign_fish(catch_id: StringName) -> bool:
|
|||
not catch_id.is_empty()
|
||||
and _fish_inventory != null
|
||||
and _fish_inventory.contains_catch_id(catch_id)
|
||||
and (
|
||||
_inventory_layout == null
|
||||
or _inventory_layout.get_container(
|
||||
PlayerInventoryLayout.EntryKind.CATCH, catch_id
|
||||
) >= 0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
519
inventory/player_inventory_layout.gd
Normal file
519
inventory/player_inventory_layout.gd
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
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 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 _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) -> 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.INVENTORY) >= 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 _is_entry_in_container(item_key(item_id), InventoryContainer.INVENTORY)
|
||||
|
||||
|
||||
func is_catch_in_inventory(catch_id: StringName) -> bool:
|
||||
return _is_entry_in_container(catch_key(catch_id), InventoryContainer.INVENTORY)
|
||||
|
||||
|
||||
func is_item_carried(item_id: StringName) -> bool:
|
||||
return _is_entry_carried(item_key(item_id))
|
||||
|
||||
|
||||
func is_catch_carried(catch_id: StringName) -> bool:
|
||||
return _is_entry_carried(catch_key(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 := _entry_key(kind, identity)
|
||||
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 := _entry_key(kind, identity)
|
||||
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 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,
|
||||
)
|
||||
|
||||
|
||||
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_to_first_free(
|
||||
int(entry.get("kind", -1)) as EntryKind,
|
||||
StringName(str(entry.get("identity", ""))),
|
||||
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", "")),
|
||||
"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 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 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,
|
||||
"container": container,
|
||||
"slot": slot,
|
||||
}
|
||||
_placements = restored
|
||||
_reconcile(true)
|
||||
return true
|
||||
|
||||
|
||||
func reset_to_defaults() -> void:
|
||||
var capacity_changed := _backpack_level != 0
|
||||
_backpack_level = 0
|
||||
_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 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 _is_entry_in_container(key: String, container: InventoryContainer) -> bool:
|
||||
var placement: Dictionary = _placements.get(key, {})
|
||||
return int(placement.get("container", -1)) == int(container)
|
||||
|
||||
|
||||
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 _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
|
||||
var key := item_key(owned.item_id)
|
||||
expected[key] = {"kind": EntryKind.ITEM, "identity": owned.item_id}
|
||||
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 target_container := InventoryContainer.INVENTORY
|
||||
var 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 _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():
|
||||
return slot
|
||||
return -1
|
||||
|
||||
|
||||
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
|
||||
1
inventory/player_inventory_layout.gd.uid
Normal file
1
inventory/player_inventory_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c5cku54c6qgmw
|
||||
Loading…
Add table
Add a link
Reference in a new issue