feat: add persistent inventory storage slots
This commit is contained in:
parent
6db6fbba7f
commit
a88e76848b
13 changed files with 875 additions and 65 deletions
|
|
@ -64,6 +64,10 @@ static func get_stock_item_ids() -> Array[StringName]:
|
|||
return ITEM_ORDER.duplicate()
|
||||
|
||||
|
||||
static func get_stock_order_index(item_id: StringName) -> int:
|
||||
return ITEM_ORDER.find(item_id)
|
||||
|
||||
|
||||
static func get_rod_stock(
|
||||
catalog: ItemCatalogType,
|
||||
) -> Array[FishingRodDataType]:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ const ItemDataType = preload("res://items/item_data.gd")
|
|||
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||
|
||||
const DEFAULT_UNLOCKED_BAIT_IDS: Array[StringName] = [&"worms"]
|
||||
const MIN_STORAGE_SLOT_COUNT: int = 15
|
||||
const MAX_STORAGE_SLOT_INDEX: int = 254
|
||||
|
||||
enum StorageGroup {
|
||||
NONE,
|
||||
EQUIPMENT,
|
||||
ITEMS,
|
||||
}
|
||||
|
||||
signal contents_changed
|
||||
|
||||
|
|
@ -31,6 +39,7 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
|
|||
var owned := OwnedItemType.new()
|
||||
owned.item_id = item_id
|
||||
owned.quantity = quantity
|
||||
owned.storage_slot = _first_available_storage_slot(item, _items)
|
||||
_items.append(owned)
|
||||
if item.is_bait() and not _unlocked_bait_ids.has(item_id):
|
||||
_unlocked_bait_ids.append(item_id)
|
||||
|
|
@ -92,6 +101,42 @@ func get_all_items() -> Array[OwnedItemType]:
|
|||
return result
|
||||
|
||||
|
||||
func get_storage_slot(item_id: StringName) -> int:
|
||||
var owned: OwnedItemType = get_owned_item(item_id)
|
||||
return owned.storage_slot if owned != null else -1
|
||||
|
||||
|
||||
func move_item_to_storage_slot(item_id: StringName, target_slot: int) -> bool:
|
||||
if target_slot < 0 or target_slot > MAX_STORAGE_SLOT_INDEX:
|
||||
return false
|
||||
var owned: OwnedItemType = get_owned_item(item_id)
|
||||
var item: ItemDataType = _resolve_valid_item(item_id)
|
||||
var group: StorageGroup = _storage_group_for_item(item)
|
||||
if owned == null or group == StorageGroup.NONE:
|
||||
return false
|
||||
if owned.storage_slot == target_slot:
|
||||
return true
|
||||
var displaced: OwnedItemType
|
||||
for candidate: OwnedItemType in _items:
|
||||
if candidate == null or candidate == owned:
|
||||
continue
|
||||
var candidate_item: ItemDataType = _resolve_valid_item(
|
||||
candidate.item_id
|
||||
)
|
||||
if (
|
||||
_storage_group_for_item(candidate_item) == group
|
||||
and candidate.storage_slot == target_slot
|
||||
):
|
||||
displaced = candidate
|
||||
break
|
||||
var previous_slot: int = owned.storage_slot
|
||||
owned.storage_slot = target_slot
|
||||
if displaced != null:
|
||||
displaced.storage_slot = previous_slot
|
||||
contents_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func get_unlocked_bait_ids() -> Array[StringName]:
|
||||
return _unlocked_bait_ids.duplicate()
|
||||
|
||||
|
|
@ -146,11 +191,80 @@ func replace_all_items(items: Array[OwnedItemType]) -> bool:
|
|||
return false
|
||||
seen[owned.item_id] = true
|
||||
validated.append(owned.duplicate_record())
|
||||
_normalize_storage_slots(validated)
|
||||
_items = validated
|
||||
contents_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func _normalize_storage_slots(items: Array[OwnedItemType]) -> void:
|
||||
var occupied: Dictionary[String, bool] = {}
|
||||
for owned: OwnedItemType in items:
|
||||
var item: ItemDataType = _resolve_valid_item(owned.item_id)
|
||||
var group: StorageGroup = _storage_group_for_item(item)
|
||||
if group == StorageGroup.NONE:
|
||||
owned.storage_slot = -1
|
||||
continue
|
||||
var key := _storage_slot_key(group, owned.storage_slot)
|
||||
if (
|
||||
owned.storage_slot < 0
|
||||
or owned.storage_slot > MAX_STORAGE_SLOT_INDEX
|
||||
or occupied.has(key)
|
||||
):
|
||||
owned.storage_slot = -1
|
||||
continue
|
||||
occupied[key] = true
|
||||
for owned: OwnedItemType in items:
|
||||
if owned.storage_slot >= 0:
|
||||
continue
|
||||
var item: ItemDataType = _resolve_valid_item(owned.item_id)
|
||||
var group: StorageGroup = _storage_group_for_item(item)
|
||||
if group == StorageGroup.NONE:
|
||||
continue
|
||||
for slot_index: int in range(MAX_STORAGE_SLOT_INDEX + 1):
|
||||
var key := _storage_slot_key(group, slot_index)
|
||||
if occupied.has(key):
|
||||
continue
|
||||
owned.storage_slot = slot_index
|
||||
occupied[key] = true
|
||||
break
|
||||
|
||||
|
||||
func _first_available_storage_slot(
|
||||
item: ItemDataType,
|
||||
items: Array[OwnedItemType],
|
||||
) -> int:
|
||||
var group: StorageGroup = _storage_group_for_item(item)
|
||||
if group == StorageGroup.NONE:
|
||||
return -1
|
||||
var occupied: Dictionary[int, bool] = {}
|
||||
for owned: OwnedItemType in items:
|
||||
var owned_item: ItemDataType = _resolve_valid_item(owned.item_id)
|
||||
if (
|
||||
_storage_group_for_item(owned_item) == group
|
||||
and owned.storage_slot >= 0
|
||||
):
|
||||
occupied[owned.storage_slot] = true
|
||||
for slot_index: int in range(MAX_STORAGE_SLOT_INDEX + 1):
|
||||
if not occupied.has(slot_index):
|
||||
return slot_index
|
||||
return -1
|
||||
|
||||
|
||||
func _storage_group_for_item(item: ItemDataType) -> StorageGroup:
|
||||
if item == null or item.is_bait() or item.is_lure():
|
||||
return StorageGroup.NONE
|
||||
return (
|
||||
StorageGroup.ITEMS
|
||||
if item.category == ItemDataType.Category.CONSUMABLE
|
||||
else StorageGroup.EQUIPMENT
|
||||
)
|
||||
|
||||
|
||||
func _storage_slot_key(group: StorageGroup, slot_index: int) -> String:
|
||||
return "%d:%d" % [int(group), slot_index]
|
||||
|
||||
|
||||
func _resolve_valid_item(item_id: StringName) -> ItemDataType:
|
||||
if _catalog == null:
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
class_name OwnedItem
|
||||
extends Resource
|
||||
|
||||
const MAX_STORAGE_SLOT_INDEX: int = 254
|
||||
|
||||
@export var item_id: StringName
|
||||
@export var quantity: int = 1
|
||||
@export var storage_slot: int = -1
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not item_id.is_empty() and quantity > 0
|
||||
return (
|
||||
not item_id.is_empty()
|
||||
and quantity > 0
|
||||
and storage_slot >= -1
|
||||
and storage_slot <= MAX_STORAGE_SLOT_INDEX
|
||||
)
|
||||
|
||||
|
||||
func duplicate_record():
|
||||
|
|
@ -17,4 +25,5 @@ func to_save_dict() -> Dictionary:
|
|||
return {
|
||||
"item_id": String(item_id),
|
||||
"quantity": quantity,
|
||||
"storage_slot": storage_slot,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -793,6 +793,11 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
var owned := OwnedItemType.new()
|
||||
owned.item_id = item_id
|
||||
owned.quantity = quantity
|
||||
owned.storage_slot = _read_integer(
|
||||
item_record.get("storage_slot", -1),
|
||||
-1,
|
||||
PlayerBagType.MAX_STORAGE_SLOT_INDEX,
|
||||
)
|
||||
seen_items[item_id] = true
|
||||
snapshot.bag_items.append(owned)
|
||||
|
||||
|
|
|
|||
67
tests/inventory_storage_persistence_validation.gd
Normal file
67
tests/inventory_storage_persistence_validation.gd
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var player := main.get("_player") as Player
|
||||
assert(save_manager != null and player != null)
|
||||
assert(save_manager.initialize_new_game())
|
||||
save_manager.set_autosave_enabled(true)
|
||||
assert(player.bag.add_item(&"art_kit"))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
|
||||
assert(player.bag.move_item_to_storage_slot(&"coffee", 12))
|
||||
assert(save_manager.save_now())
|
||||
|
||||
var save_file := FileAccess.open(
|
||||
str(save_manager.get("_save_path")),
|
||||
FileAccess.READ,
|
||||
)
|
||||
assert(save_file != null)
|
||||
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var records: Array = (parsed as Dictionary)["bag"]["items"]
|
||||
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
|
||||
assert(_saved_slot(records, &"coffee") == 12)
|
||||
|
||||
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
|
||||
assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
|
||||
assert(save_manager.load_player_data())
|
||||
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
||||
assert(player.bag.get_storage_slot(&"coffee") == 12)
|
||||
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
await create_timer(0.1).timeout
|
||||
print("Inventory storage persistence validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _saved_slot(records: Array, item_id: StringName) -> int:
|
||||
for value: Variant in records:
|
||||
if (
|
||||
typeof(value) == TYPE_DICTIONARY
|
||||
and str((value as Dictionary).get("item_id", ""))
|
||||
== String(item_id)
|
||||
):
|
||||
return int((value as Dictionary).get("storage_slot", -1))
|
||||
return -1
|
||||
1
tests/inventory_storage_persistence_validation.gd.uid
Normal file
1
tests/inventory_storage_persistence_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bwp2jhs116hmw
|
||||
172
tests/inventory_storage_validation.gd
Normal file
172
tests/inventory_storage_validation.gd
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
extends SceneTree
|
||||
|
||||
const BagItemSpriteType = preload(
|
||||
"res://ui/components/bubble_menu/bag_item_sprite.gd"
|
||||
)
|
||||
const BagStorageSlotType = preload(
|
||||
"res://ui/components/bubble_menu/bag_storage_slot.gd"
|
||||
)
|
||||
const Catalog: ItemCatalog = preload(
|
||||
"res://items/catalog/item_catalog.tres"
|
||||
)
|
||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||
|
||||
const EQUIPMENT_IDS: Array[StringName] = [
|
||||
&"basic_fishing_rod",
|
||||
&"art_kit",
|
||||
&"crab_net",
|
||||
&"magnet",
|
||||
]
|
||||
const ITEM_IDS: Array[StringName] = [
|
||||
&"coffee",
|
||||
&"energy_drink",
|
||||
&"snack",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var bag := PlayerBagType.new()
|
||||
bag.setup(Catalog)
|
||||
for item_id: StringName in EQUIPMENT_IDS + ITEM_IDS:
|
||||
assert(bag.add_item(item_id))
|
||||
for index: int in EQUIPMENT_IDS.size():
|
||||
assert(bag.get_storage_slot(EQUIPMENT_IDS[index]) == index)
|
||||
for index: int in ITEM_IDS.size():
|
||||
assert(bag.get_storage_slot(ITEM_IDS[index]) == index)
|
||||
|
||||
assert(bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
|
||||
assert(bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
||||
assert(bag.move_item_to_storage_slot(&"crab_net", 14))
|
||||
assert(bag.get_storage_slot(&"crab_net") == 14)
|
||||
assert(bag.get_storage_slot(&"basic_fishing_rod") == 2)
|
||||
var saved_record: Dictionary = bag.get_owned_item(
|
||||
&"crab_net"
|
||||
).to_save_dict()
|
||||
assert(int(saved_record.get("storage_slot", -1)) == 14)
|
||||
|
||||
var legacy_equipment := OwnedItemType.new()
|
||||
legacy_equipment.item_id = &"basic_fishing_rod"
|
||||
var legacy_item := OwnedItemType.new()
|
||||
legacy_item.item_id = &"coffee"
|
||||
var legacy_records: Array[OwnedItemType] = [
|
||||
legacy_equipment,
|
||||
legacy_item,
|
||||
]
|
||||
var legacy_bag := PlayerBagType.new()
|
||||
legacy_bag.setup(Catalog)
|
||||
assert(legacy_bag.replace_all_items(legacy_records))
|
||||
assert(legacy_bag.get_storage_slot(&"basic_fishing_rod") == 0)
|
||||
assert(legacy_bag.get_storage_slot(&"coffee") == 0)
|
||||
legacy_bag.free()
|
||||
|
||||
var menu := PlayerMenuScene.instantiate() as PlayerMenu
|
||||
root.add_child(menu)
|
||||
await process_frame
|
||||
menu.set("_bag", bag)
|
||||
var hotbar := PlayerHotbarType.new()
|
||||
hotbar.setup(bag, Catalog)
|
||||
menu.set("_hotbar", hotbar)
|
||||
menu.set("_item_catalog", Catalog)
|
||||
menu.set("_bag_view", PlayerMenuType.BagView.EQUIPMENT)
|
||||
menu.visible = true
|
||||
menu.call("_show_section_immediate", PlayerMenuType.Section.BAG)
|
||||
menu.call("_set_content_interactive", true)
|
||||
menu.call("_refresh_bag")
|
||||
await process_frame
|
||||
_validate_grid(menu, EQUIPMENT_IDS.size())
|
||||
|
||||
var item_nodes: Dictionary = menu.get("_bag_item_nodes")
|
||||
var crab_node := item_nodes.get(&"crab_net") as BagItemSpriteType
|
||||
assert(crab_node != null)
|
||||
menu.set(
|
||||
"_controller_ownership",
|
||||
PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
)
|
||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
||||
crab_node.grab_focus()
|
||||
assert(bool(menu.call("_try_begin_controller_storage_placement")))
|
||||
await process_frame
|
||||
assert(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
|
||||
)
|
||||
var slots: Array = menu.get("_bag_slot_nodes")
|
||||
var bottom_slot := slots[12] as BagStorageSlotType
|
||||
bottom_slot.grab_focus()
|
||||
var down := InputEventAction.new()
|
||||
down.action = &"ui_down"
|
||||
down.pressed = true
|
||||
assert(bool(menu.call("_handle_controller_ownership_input", down)))
|
||||
assert(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.HOTBAR_PLACEMENT
|
||||
)
|
||||
var up := InputEventAction.new()
|
||||
up.action = &"ui_up"
|
||||
up.pressed = true
|
||||
assert(bool(menu.call("_handle_controller_ownership_input", up)))
|
||||
assert(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
|
||||
)
|
||||
await process_frame
|
||||
assert(root.gui_get_focus_owner() == bottom_slot)
|
||||
assert(StringName(menu.get("_controller_storage_identity")) == &"crab_net")
|
||||
var target_slot := slots[7] as BagStorageSlotType
|
||||
target_slot.grab_focus()
|
||||
menu.call("_confirm_controller_storage_placement")
|
||||
await process_frame
|
||||
assert(bag.get_storage_slot(&"crab_net") == 7)
|
||||
assert(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST
|
||||
)
|
||||
|
||||
menu.call("_show_bag_view", PlayerMenuType.BagView.CONSUMABLES)
|
||||
await process_frame
|
||||
_validate_grid(menu, ITEM_IDS.size())
|
||||
menu.call("_on_bag_item_dropped", &"coffee", 12)
|
||||
await process_frame
|
||||
assert(bag.get_storage_slot(&"coffee") == 12)
|
||||
|
||||
menu.queue_free()
|
||||
hotbar.free()
|
||||
bag.free()
|
||||
await process_frame
|
||||
print("Inventory storage validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_grid(menu: PlayerMenu, expected_items: int) -> void:
|
||||
var slots: Array = menu.get("_bag_slot_nodes")
|
||||
assert(slots.size() == PlayerBagType.MIN_STORAGE_SLOT_COUNT)
|
||||
var seen_positions: Dictionary[Vector2, bool] = {}
|
||||
for index: int in slots.size():
|
||||
var slot := slots[index] as BagStorageSlotType
|
||||
assert(slot != null)
|
||||
assert(slot.storage_slot_index == index)
|
||||
assert(not seen_positions.has(slot.position))
|
||||
seen_positions[slot.position] = true
|
||||
var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
|
||||
assert(normal != null)
|
||||
assert(normal.bg_color.a >= 0.7)
|
||||
assert((slots[1] as Control).position.x > (slots[0] as Control).position.x)
|
||||
assert((slots[5] as Control).position.y > (slots[0] as Control).position.y)
|
||||
var item_nodes := menu.get("_bag_item_nodes") as Dictionary
|
||||
assert(
|
||||
item_nodes.size() == expected_items,
|
||||
"expected %d visible bag items, found %d: %s" % [
|
||||
expected_items,
|
||||
item_nodes.size(),
|
||||
str(item_nodes.keys()),
|
||||
],
|
||||
)
|
||||
1
tests/inventory_storage_validation.gd.uid
Normal file
1
tests/inventory_storage_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://w6e06q8yfwp6
|
||||
50
tests/tackle_order_validation.gd
Normal file
50
tests/tackle_order_validation.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
extends SceneTree
|
||||
|
||||
const Catalog: ItemCatalog = preload("res://items/catalog/item_catalog.tres")
|
||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||
|
||||
const EXPECTED_BAIT_ORDER: Array[StringName] = [
|
||||
&"worms",
|
||||
&"snails",
|
||||
&"shrimp",
|
||||
&"squid_chunks",
|
||||
&"whole_sardine",
|
||||
&"whole_anchovy",
|
||||
&"luminous_roe",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var player_menu := PlayerMenuType.new() as PlayerMenu
|
||||
player_menu.set("_item_catalog", Catalog)
|
||||
var tackle_items: Array[OwnedItem] = []
|
||||
for item_id: StringName in [
|
||||
&"luminous_roe",
|
||||
&"whole_anchovy",
|
||||
&"shrimp",
|
||||
&"snails",
|
||||
&"whole_sardine",
|
||||
&"squid_chunks",
|
||||
&"worms",
|
||||
]:
|
||||
var owned := OwnedItem.new()
|
||||
owned.item_id = item_id
|
||||
owned.quantity = 1
|
||||
tackle_items.append(owned)
|
||||
tackle_items.sort_custom(
|
||||
func(first: OwnedItem, second: OwnedItem) -> bool:
|
||||
return bool(player_menu.call(
|
||||
"_sort_tackle_items", first, second
|
||||
))
|
||||
)
|
||||
var sorted_ids: Array[StringName] = []
|
||||
for owned: OwnedItem in tackle_items:
|
||||
sorted_ids.append(owned.item_id)
|
||||
assert(sorted_ids == EXPECTED_BAIT_ORDER)
|
||||
player_menu.free()
|
||||
print("Tackle order validation: PASS")
|
||||
quit()
|
||||
1
tests/tackle_order_validation.gd.uid
Normal file
1
tests/tackle_order_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://27tkdvxgkv3y
|
||||
72
ui/components/bubble_menu/bag_storage_slot.gd
Normal file
72
ui/components/bubble_menu/bag_storage_slot.gd
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
class_name BagStorageSlot
|
||||
extends Button
|
||||
|
||||
signal bag_item_dropped(item_id: StringName, slot_index: int)
|
||||
|
||||
var storage_slot_index: int = -1
|
||||
var _preview: TextureRect
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
focus_mode = Control.FOCUS_NONE
|
||||
toggle_mode = false
|
||||
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
_apply_style()
|
||||
_preview = TextureRect.new()
|
||||
_preview.name = "PlacementPreview"
|
||||
_preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_preview.offset_left = 8.0
|
||||
_preview.offset_top = 7.0
|
||||
_preview.offset_right = -8.0
|
||||
_preview.offset_bottom = -7.0
|
||||
_preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_preview.visible = false
|
||||
add_child(_preview)
|
||||
|
||||
|
||||
func configure(slot_index: int) -> void:
|
||||
storage_slot_index = slot_index
|
||||
name = "BagStorageSlot%d" % slot_index
|
||||
accessibility_name = "storage slot %d" % (slot_index + 1)
|
||||
|
||||
|
||||
func set_placement_preview(texture: Texture2D, active: bool) -> void:
|
||||
if _preview == null:
|
||||
return
|
||||
_preview.texture = texture
|
||||
_preview.visible = active and texture != null
|
||||
z_index = 2 if _preview.visible else 0
|
||||
|
||||
|
||||
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||
return (
|
||||
typeof(data) == TYPE_DICTIONARY
|
||||
and str((data as Dictionary).get("kind", "")) == "bag_item"
|
||||
and not str((data as Dictionary).get("item_id", "")).is_empty()
|
||||
)
|
||||
|
||||
|
||||
func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
||||
if not _can_drop_data(Vector2.ZERO, data):
|
||||
return
|
||||
bag_item_dropped.emit(
|
||||
StringName(str((data as Dictionary)["item_id"])),
|
||||
storage_slot_index,
|
||||
)
|
||||
|
||||
|
||||
func _apply_style() -> void:
|
||||
var normal := UtilityPageStyle.rounded_style(
|
||||
Color(UtilityPageStyle.OCEAN_FIELD, 0.72),
|
||||
14,
|
||||
)
|
||||
var hover := UtilityPageStyle.rounded_style(
|
||||
Color(UtilityPageStyle.OCEAN_SELECTED, 0.68),
|
||||
14,
|
||||
)
|
||||
for state: StringName in [&"normal", &"pressed", &"focus", &"disabled"]:
|
||||
add_theme_stylebox_override(state, normal)
|
||||
add_theme_stylebox_override(&"hover", hover)
|
||||
1
ui/components/bubble_menu/bag_storage_slot.gd.uid
Normal file
1
ui/components/bubble_menu/bag_storage_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ofac40nwf1oa
|
||||
|
|
@ -9,6 +9,9 @@ const FishQualityType = preload("res://fish/fish_quality.gd")
|
|||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||
const FishingShopStockType = preload(
|
||||
"res://economy/fishing_shop_stock.gd"
|
||||
)
|
||||
const NetworkSessionType = preload("res://network/network_session.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const InventoryNotepadType = preload(
|
||||
|
|
@ -72,6 +75,9 @@ const BagItemSpriteType = preload(
|
|||
const BagItemSpriteScene = preload(
|
||||
"res://ui/components/bubble_menu/bag_item_sprite.tscn"
|
||||
)
|
||||
const BagStorageSlotType = preload(
|
||||
"res://ui/components/bubble_menu/bag_storage_slot.gd"
|
||||
)
|
||||
const LogbookEntryType = preload(
|
||||
"res://ui/components/bubble_menu/logbook_entry.gd"
|
||||
)
|
||||
|
|
@ -140,6 +146,7 @@ enum ControllerOwnership {
|
|||
SORT_FILTER,
|
||||
HOTBAR_MANAGEMENT,
|
||||
HOTBAR_PLACEMENT,
|
||||
STORAGE_PLACEMENT,
|
||||
PAGE_CONTENT,
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +161,8 @@ const INVENTORY_NOTEPAD_SIZE := Vector2(278.0, 484.0)
|
|||
const INVENTORY_MAIN_CORNER_RADIUS: int = 58
|
||||
const INVENTORY_INNER_CORNER_RADIUS: int = 45
|
||||
const TACKLE_GRID_COLUMNS: int = 3
|
||||
const BAG_STORAGE_COLUMNS: int = 5
|
||||
const BAG_STORAGE_MINIMUM_SLOTS: int = 15
|
||||
const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
|
||||
const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42
|
||||
|
||||
|
|
@ -325,7 +334,9 @@ var _controller_hotbar_assignment_kind: PlayerHotbarType.AssignmentKind = (
|
|||
PlayerHotbarType.AssignmentKind.EMPTY
|
||||
)
|
||||
var _controller_hotbar_identity: StringName
|
||||
var _controller_storage_identity: StringName
|
||||
var _controller_previous_hotbar_slot: int = 0
|
||||
var _controller_previous_storage_slot: int = 0
|
||||
var _controller_accept_held: bool = false
|
||||
var _controller_accept_hold_elapsed: float = 0.0
|
||||
var _content_interactive_enabled: bool = false
|
||||
|
|
@ -367,7 +378,7 @@ var _fish_nodes: Dictionary[StringName, CoolerFishSpriteType] = {}
|
|||
var _cooler_slot_nodes: Array[Panel] = []
|
||||
var _sorted_catches: Array[FishCatchType] = []
|
||||
var _bag_item_nodes: Dictionary[StringName, BagItemSpriteType] = {}
|
||||
var _bag_slot_nodes: Array[Panel] = []
|
||||
var _bag_slot_nodes: Array[BagStorageSlotType] = []
|
||||
var _sorted_bag_items: Array[OwnedItemType] = []
|
||||
var _bag_drag_active: bool = false
|
||||
var _motion_elapsed: float = 0.0
|
||||
|
|
@ -693,16 +704,45 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
||||
if event.is_action_pressed("ui_up"):
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
if accept_pressed:
|
||||
if _hotbar != null:
|
||||
_hotbar.clear_slot(_hotbar.get_selected_slot())
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if event.is_action_pressed("ui_up"):
|
||||
if _return_controller_hotbar_placement_to_storage():
|
||||
return true
|
||||
_release_controller_ownership(true, true)
|
||||
return true
|
||||
if accept_pressed:
|
||||
_confirm_controller_hotbar_placement()
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.STORAGE_PLACEMENT:
|
||||
if accept_pressed:
|
||||
_confirm_controller_storage_placement()
|
||||
return true
|
||||
if accept_event:
|
||||
return true
|
||||
if (
|
||||
event.is_action_pressed("ui_down")
|
||||
and _controller_storage_focus_is_on_last_row()
|
||||
):
|
||||
var focused_slot := (
|
||||
get_viewport().gui_get_focus_owner() as BagStorageSlotType
|
||||
)
|
||||
if focused_slot != null:
|
||||
_controller_previous_storage_slot = (
|
||||
focused_slot.storage_slot_index
|
||||
)
|
||||
return _begin_controller_hotbar_placement_for_item(
|
||||
_controller_storage_identity
|
||||
)
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.SORT_FILTER:
|
||||
|
|
@ -886,13 +926,26 @@ func _controller_focus_is_on_last_inventory_row() -> bool:
|
|||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node == null:
|
||||
return false
|
||||
var item_index: int = _sorted_bag_items.find_custom(
|
||||
func(owned: OwnedItemType) -> bool:
|
||||
return owned.item_id == item_node.item_id
|
||||
if _bag == null:
|
||||
var legacy_index: int = _sorted_bag_items.find_custom(
|
||||
func(owned: OwnedItemType) -> bool:
|
||||
return owned.item_id == item_node.item_id
|
||||
)
|
||||
return (
|
||||
legacy_index >= 0
|
||||
and legacy_index + BAG_STORAGE_COLUMNS
|
||||
>= _sorted_bag_items.size()
|
||||
)
|
||||
var item_slot: int = (
|
||||
_bag.get_storage_slot(item_node.item_id)
|
||||
)
|
||||
var maximum_slot: int = -1
|
||||
for owned: OwnedItemType in _sorted_bag_items:
|
||||
maximum_slot = maxi(maximum_slot, owned.storage_slot)
|
||||
return (
|
||||
item_index >= 0
|
||||
and item_index + 3 >= _sorted_bag_items.size()
|
||||
item_slot >= 0
|
||||
and item_slot / BAG_STORAGE_COLUMNS
|
||||
>= maximum_slot / BAG_STORAGE_COLUMNS
|
||||
)
|
||||
return false
|
||||
|
||||
|
|
@ -956,6 +1009,28 @@ func _try_begin_controller_hotbar_placement() -> bool:
|
|||
identity = item_node.item_id
|
||||
else:
|
||||
return false
|
||||
return _begin_controller_hotbar_placement(assignment_kind, identity)
|
||||
|
||||
|
||||
func _begin_controller_hotbar_placement_for_item(item_id: StringName) -> bool:
|
||||
var item: ItemDataType = (
|
||||
_item_catalog.get_item_by_id(item_id)
|
||||
if _item_catalog != null else null
|
||||
)
|
||||
if item == null or not item.hotbar_allowed:
|
||||
return true
|
||||
return _begin_controller_hotbar_placement(
|
||||
PlayerHotbarType.AssignmentKind.ITEM,
|
||||
item_id,
|
||||
)
|
||||
|
||||
|
||||
func _begin_controller_hotbar_placement(
|
||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
||||
identity: StringName,
|
||||
) -> bool:
|
||||
if _hotbar == null or identity.is_empty():
|
||||
return false
|
||||
_controller_source_section = _current_section
|
||||
_controller_source_identity = identity
|
||||
_controller_hotbar_assignment_kind = assignment_kind
|
||||
|
|
@ -977,6 +1052,53 @@ func _try_begin_controller_hotbar_placement() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _try_begin_controller_storage_placement() -> bool:
|
||||
if _current_section != Section.BAG or _bag == null:
|
||||
return false
|
||||
var item_node := get_viewport().gui_get_focus_owner() as BagItemSpriteType
|
||||
if item_node == null or item_node.item_id.is_empty():
|
||||
return false
|
||||
var source_slot: int = _bag.get_storage_slot(item_node.item_id)
|
||||
if source_slot < 0 or source_slot >= _bag_slot_nodes.size():
|
||||
return false
|
||||
_controller_source_section = Section.BAG
|
||||
_controller_source_identity = item_node.item_id
|
||||
_controller_storage_identity = item_node.item_id
|
||||
_controller_ownership = ControllerOwnership.STORAGE_PLACEMENT
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
_configure_bag_storage_slot_focus()
|
||||
_bag_slot_nodes[source_slot].call_deferred("grab_focus")
|
||||
return true
|
||||
|
||||
|
||||
func _confirm_controller_storage_placement() -> void:
|
||||
if (
|
||||
_controller_ownership != ControllerOwnership.STORAGE_PLACEMENT
|
||||
or _bag == null
|
||||
or _controller_storage_identity.is_empty()
|
||||
):
|
||||
return
|
||||
var slot := get_viewport().gui_get_focus_owner() as BagStorageSlotType
|
||||
if slot == null or slot.storage_slot_index < 0:
|
||||
return
|
||||
var moved: bool = _bag.move_item_to_storage_slot(
|
||||
_controller_storage_identity,
|
||||
slot.storage_slot_index,
|
||||
)
|
||||
if moved:
|
||||
_release_controller_ownership(true, false)
|
||||
|
||||
|
||||
func _controller_storage_focus_is_on_last_row() -> bool:
|
||||
var slot := get_viewport().gui_get_focus_owner() as BagStorageSlotType
|
||||
if slot == null:
|
||||
return false
|
||||
return (
|
||||
slot.storage_slot_index / BAG_STORAGE_COLUMNS
|
||||
>= (_bag_slot_nodes.size() - 1) / BAG_STORAGE_COLUMNS
|
||||
)
|
||||
|
||||
|
||||
func _find_controller_hotbar_assignment(
|
||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
||||
identity: StringName,
|
||||
|
|
@ -1023,6 +1145,31 @@ func _confirm_controller_hotbar_placement() -> void:
|
|||
_release_controller_ownership(true, false)
|
||||
|
||||
|
||||
func _return_controller_hotbar_placement_to_storage() -> bool:
|
||||
if (
|
||||
_controller_ownership != ControllerOwnership.HOTBAR_PLACEMENT
|
||||
or _controller_source_section != Section.BAG
|
||||
or _controller_storage_identity.is_empty()
|
||||
or _bag_slot_nodes.is_empty()
|
||||
):
|
||||
return false
|
||||
if _hotbar != null:
|
||||
_hotbar.select_slot(_controller_previous_hotbar_slot)
|
||||
controller_hotbar_placement_ended.emit()
|
||||
_controller_ownership = ControllerOwnership.STORAGE_PLACEMENT
|
||||
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
||||
_controller_hotbar_identity = StringName()
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
_configure_bag_storage_slot_focus()
|
||||
var target_index: int = clampi(
|
||||
_controller_previous_storage_slot,
|
||||
0,
|
||||
_bag_slot_nodes.size() - 1,
|
||||
)
|
||||
_bag_slot_nodes[target_index].call_deferred("grab_focus")
|
||||
return true
|
||||
|
||||
|
||||
func _release_controller_ownership(
|
||||
restore_source_focus: bool,
|
||||
restore_previous_hotbar_slot: bool,
|
||||
|
|
@ -1037,6 +1184,7 @@ func _release_controller_ownership(
|
|||
_controller_source_identity = StringName()
|
||||
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
||||
_controller_hotbar_identity = StringName()
|
||||
_controller_storage_identity = StringName()
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
if prior_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if restore_previous_hotbar_slot and _hotbar != null:
|
||||
|
|
@ -1320,6 +1468,8 @@ func _consume_player_menu_back() -> bool:
|
|||
_release_controller_ownership(true, false)
|
||||
ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
_release_controller_ownership(true, true)
|
||||
ControllerOwnership.STORAGE_PLACEMENT:
|
||||
_release_controller_ownership(true, false)
|
||||
ControllerOwnership.ITEM_LIST:
|
||||
_enter_inventory_tabs_zone()
|
||||
ControllerOwnership.INVENTORY_TABS:
|
||||
|
|
@ -1773,7 +1923,10 @@ func _process(delta: float) -> void:
|
|||
>= CONTROLLER_PICKUP_HOLD_SECONDS
|
||||
):
|
||||
_cancel_controller_accept_hold()
|
||||
_try_begin_controller_hotbar_placement()
|
||||
if _current_section == Section.BAG:
|
||||
_try_begin_controller_storage_placement()
|
||||
else:
|
||||
_try_begin_controller_hotbar_placement()
|
||||
_motion_elapsed += delta
|
||||
if visible:
|
||||
_navigation_cluster.advance_motion(delta)
|
||||
|
|
@ -1907,6 +2060,11 @@ func _apply_inventory_controller_zone_focus_modes() -> void:
|
|||
regular_active
|
||||
and _controller_ownership == ControllerOwnership.SORT_FILTER
|
||||
)
|
||||
var storage_active: bool = (
|
||||
regular_active
|
||||
and _current_section == Section.BAG
|
||||
and _controller_ownership == ControllerOwnership.STORAGE_PLACEMENT
|
||||
)
|
||||
for sort_control: Control in [
|
||||
_cooler_sort_option,
|
||||
_cooler_sort_direction,
|
||||
|
|
@ -1952,6 +2110,27 @@ func _apply_inventory_controller_zone_focus_modes() -> void:
|
|||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
item_node.modulate.a = (
|
||||
0.24
|
||||
if (
|
||||
storage_active
|
||||
and item_node.item_id == _controller_storage_identity
|
||||
)
|
||||
else 1.0
|
||||
)
|
||||
for slot: BagStorageSlotType in _bag_slot_nodes:
|
||||
if not is_instance_valid(slot):
|
||||
continue
|
||||
slot.focus_mode = (
|
||||
Control.FOCUS_ALL if storage_active else Control.FOCUS_NONE
|
||||
)
|
||||
var preview_active: bool = (
|
||||
storage_active and slot.has_focus()
|
||||
)
|
||||
slot.set_placement_preview(
|
||||
_controller_storage_texture(),
|
||||
preview_active,
|
||||
)
|
||||
for button: Button in _tackle_item_buttons.values():
|
||||
button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
|
|
@ -2124,8 +2303,8 @@ func _refresh_tackle_box() -> void:
|
|||
)
|
||||
if item != null and item.category == ItemDataType.Category.LURE:
|
||||
lure_items.append(owned)
|
||||
bait_items.sort_custom(_sort_bag_items)
|
||||
lure_items.sort_custom(_sort_bag_items)
|
||||
bait_items.sort_custom(_sort_tackle_items)
|
||||
lure_items.sort_custom(_sort_tackle_items)
|
||||
_populate_tackle_column(bait_items, _bait_item_list)
|
||||
_populate_tackle_column(lure_items, _lure_item_list)
|
||||
if (
|
||||
|
|
@ -3411,10 +3590,10 @@ func _refresh_bag() -> void:
|
|||
):
|
||||
filtered_items.append(owned)
|
||||
owned_items = filtered_items
|
||||
owned_items.sort_custom(_sort_bag_items)
|
||||
owned_items.sort_custom(_sort_bag_storage_items)
|
||||
_sorted_bag_items = owned_items
|
||||
_bag_empty.visible = owned_items.is_empty()
|
||||
_bag_empty_state.visible = owned_items.is_empty()
|
||||
_bag_empty.visible = false
|
||||
_bag_empty_state.visible = false
|
||||
_bag_empty_state.text = (
|
||||
"No equipment in your Bag."
|
||||
if _bag_view == BagView.EQUIPMENT
|
||||
|
|
@ -3462,9 +3641,9 @@ func _sync_bag_item_nodes(owned_items: Array[OwnedItemType]) -> void:
|
|||
0.97 + float(identity_hash % 7) * 0.01,
|
||||
)
|
||||
item_node.tooltip_text = (
|
||||
"drag to a hotbar slot."
|
||||
"drag to another storage slot or the hotbar."
|
||||
if item.hotbar_allowed
|
||||
else "this item cannot be assigned to the hotbar."
|
||||
else "drag to another storage slot."
|
||||
)
|
||||
item_node.disabled = false
|
||||
item_node.set_selected(item.item_id == _selected_bag_item_id)
|
||||
|
|
@ -3482,63 +3661,104 @@ func _sync_bag_item_nodes(owned_items: Array[OwnedItemType]) -> void:
|
|||
func _layout_bag_items() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
_bag_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var columns: int = 3
|
||||
_bag_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
var columns: int = BAG_STORAGE_COLUMNS
|
||||
var cell_size := (
|
||||
Vector2(166.0, 72.0)
|
||||
Vector2(102.0, 48.0)
|
||||
if _compact_layout
|
||||
else Vector2(246.0, 154.0)
|
||||
else Vector2(154.0, 110.0)
|
||||
)
|
||||
var slot_size := (
|
||||
Vector2(90.0, 40.0)
|
||||
if _compact_layout
|
||||
else Vector2(140.0, 96.0)
|
||||
)
|
||||
var item_size := (
|
||||
Vector2(86.0, 62.0)
|
||||
Vector2(78.0, 36.0)
|
||||
if _compact_layout
|
||||
else Vector2(138.0, 102.0)
|
||||
else Vector2(122.0, 82.0)
|
||||
)
|
||||
var origin := (
|
||||
Vector2(16.0, 4.0)
|
||||
Vector2(5.0, 4.0)
|
||||
if _compact_layout
|
||||
else Vector2(54.0, 44.0)
|
||||
else Vector2(10.0, 9.0)
|
||||
)
|
||||
_bag_item_field.custom_minimum_size = (
|
||||
Vector2(520.0, 140.0)
|
||||
if _compact_layout
|
||||
else Vector2(820.0, 240.0)
|
||||
var highest_slot: int = -1
|
||||
for owned: OwnedItemType in _sorted_bag_items:
|
||||
highest_slot = maxi(highest_slot, owned.storage_slot)
|
||||
var slot_count: int = maxi(
|
||||
BAG_STORAGE_MINIMUM_SLOTS,
|
||||
highest_slot + 1,
|
||||
)
|
||||
_sync_inventory_slot_visuals(
|
||||
_bag_item_field,
|
||||
_bag_slot_nodes,
|
||||
maxi(9, _sorted_bag_items.size()),
|
||||
slot_count = ceili(float(slot_count) / float(columns)) * columns
|
||||
var required_rows: int = ceili(float(slot_count) / float(columns))
|
||||
var content_size := Vector2(
|
||||
520.0 if _compact_layout else 788.0,
|
||||
maxf(
|
||||
152.0 if _compact_layout else 358.0,
|
||||
origin.y * 2.0 + float(required_rows) * cell_size.y,
|
||||
),
|
||||
)
|
||||
_bag_host.custom_minimum_size = content_size
|
||||
_bag_item_field.custom_minimum_size = content_size
|
||||
_sync_bag_storage_slots(
|
||||
slot_count,
|
||||
columns,
|
||||
cell_size,
|
||||
item_size,
|
||||
slot_size,
|
||||
origin,
|
||||
18.0,
|
||||
)
|
||||
for index: int in _sorted_bag_items.size():
|
||||
var owned: OwnedItemType = _sorted_bag_items[index]
|
||||
for owned: OwnedItemType in _sorted_bag_items:
|
||||
var item_node := _bag_item_nodes.get(
|
||||
owned.item_id
|
||||
) as BagItemSpriteType
|
||||
if item_node == null:
|
||||
if item_node == null or owned.storage_slot < 0:
|
||||
continue
|
||||
var identity_hash: int = absi(String(owned.item_id).hash())
|
||||
var column: int = index % columns
|
||||
var row: int = floori(float(index) / float(columns))
|
||||
var stable_offset := Vector2(
|
||||
float(identity_hash % 13) - 6.0,
|
||||
float(floori(float(identity_hash) / 19.0) % 11) - 5.0,
|
||||
var column: int = owned.storage_slot % columns
|
||||
var row: int = floori(float(owned.storage_slot) / float(columns))
|
||||
var slot_position := origin + Vector2(
|
||||
float(column) * cell_size.x,
|
||||
float(row) * cell_size.y,
|
||||
)
|
||||
var lane_offset: float = 18.0 if row % 2 == 1 else 0.0
|
||||
item_node.custom_minimum_size = item_size
|
||||
item_node.size = item_size
|
||||
item_node.position = (
|
||||
origin
|
||||
+ Vector2(
|
||||
float(column) * cell_size.x + lane_offset,
|
||||
float(row) * cell_size.y,
|
||||
)
|
||||
+ stable_offset
|
||||
slot_position + (slot_size - item_size) * 0.5
|
||||
)
|
||||
item_node.z_index = 1
|
||||
|
||||
|
||||
func _sync_bag_storage_slots(
|
||||
slot_count: int,
|
||||
columns: int,
|
||||
cell_size: Vector2,
|
||||
slot_size: Vector2,
|
||||
origin: Vector2,
|
||||
) -> void:
|
||||
for slot: BagStorageSlotType in _bag_slot_nodes:
|
||||
if is_instance_valid(slot):
|
||||
slot.queue_free()
|
||||
_bag_slot_nodes.clear()
|
||||
for slot_index: int in slot_count:
|
||||
var row: int = floori(float(slot_index) / float(columns))
|
||||
var column: int = slot_index % columns
|
||||
var slot := BagStorageSlotType.new()
|
||||
slot.configure(slot_index)
|
||||
slot.size = slot_size
|
||||
slot.custom_minimum_size = slot_size
|
||||
slot.position = origin + Vector2(
|
||||
float(column) * cell_size.x,
|
||||
float(row) * cell_size.y,
|
||||
)
|
||||
slot.bag_item_dropped.connect(_on_bag_item_dropped)
|
||||
slot.pressed.connect(_on_bag_storage_slot_pressed.bind(slot))
|
||||
slot.focus_entered.connect(
|
||||
_on_bag_storage_slot_focus_entered.bind(slot)
|
||||
)
|
||||
_bag_item_field.add_child(slot)
|
||||
_bag_item_field.move_child(slot, 0)
|
||||
_bag_slot_nodes.append(slot)
|
||||
_configure_bag_storage_slot_focus()
|
||||
|
||||
|
||||
func _sync_inventory_slot_visuals(
|
||||
|
|
@ -3596,30 +3816,44 @@ func _configure_bag_item_focus() -> void:
|
|||
active_tab.focus_neighbor_bottom = active_tab.get_path_to(
|
||||
controls.front()
|
||||
)
|
||||
for index: int in controls.size():
|
||||
var control: BagItemSpriteType = controls[index]
|
||||
var column: int = index % 3
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
|
||||
|
||||
func _configure_bag_storage_slot_focus() -> void:
|
||||
for index: int in _bag_slot_nodes.size():
|
||||
var slot: BagStorageSlotType = _bag_slot_nodes[index]
|
||||
if not is_instance_valid(slot):
|
||||
continue
|
||||
var column: int = index % BAG_STORAGE_COLUMNS
|
||||
var left_index: int = index - 1 if column > 0 else index
|
||||
var right_index: int = (
|
||||
index + 1
|
||||
if column < 2 and index + 1 < controls.size()
|
||||
if (
|
||||
column < BAG_STORAGE_COLUMNS - 1
|
||||
and index + 1 < _bag_slot_nodes.size()
|
||||
)
|
||||
else index
|
||||
)
|
||||
control.focus_neighbor_left = control.get_path_to(
|
||||
controls[left_index]
|
||||
var top_index: int = (
|
||||
index - BAG_STORAGE_COLUMNS
|
||||
if index >= BAG_STORAGE_COLUMNS else index
|
||||
)
|
||||
control.focus_neighbor_right = control.get_path_to(
|
||||
controls[right_index]
|
||||
var bottom_index: int = (
|
||||
index + BAG_STORAGE_COLUMNS
|
||||
if index + BAG_STORAGE_COLUMNS < _bag_slot_nodes.size()
|
||||
else index
|
||||
)
|
||||
control.focus_neighbor_top = (
|
||||
control.get_path_to(control)
|
||||
if index < 3
|
||||
else control.get_path_to(controls[index - 3])
|
||||
slot.focus_neighbor_left = slot.get_path_to(
|
||||
_bag_slot_nodes[left_index]
|
||||
)
|
||||
control.focus_neighbor_bottom = control.get_path_to(
|
||||
controls[index + 3]
|
||||
if index + 3 < controls.size()
|
||||
else control
|
||||
slot.focus_neighbor_right = slot.get_path_to(
|
||||
_bag_slot_nodes[right_index]
|
||||
)
|
||||
slot.focus_neighbor_top = slot.get_path_to(
|
||||
_bag_slot_nodes[top_index]
|
||||
)
|
||||
slot.focus_neighbor_bottom = slot.get_path_to(
|
||||
_bag_slot_nodes[bottom_index]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3643,6 +3877,28 @@ func _sort_bag_items(a: OwnedItemType, b: OwnedItemType) -> bool:
|
|||
return item_a.display_name.naturalnocasecmp_to(item_b.display_name) < 0
|
||||
|
||||
|
||||
func _sort_bag_storage_items(a: OwnedItemType, b: OwnedItemType) -> bool:
|
||||
if a.storage_slot >= 0 and b.storage_slot >= 0:
|
||||
return a.storage_slot < b.storage_slot
|
||||
if a.storage_slot >= 0:
|
||||
return true
|
||||
if b.storage_slot >= 0:
|
||||
return false
|
||||
return _sort_bag_items(a, b)
|
||||
|
||||
|
||||
func _sort_tackle_items(a: OwnedItemType, b: OwnedItemType) -> bool:
|
||||
var order_a := FishingShopStockType.get_stock_order_index(a.item_id)
|
||||
var order_b := FishingShopStockType.get_stock_order_index(b.item_id)
|
||||
if order_a >= 0 and order_b >= 0 and order_a != order_b:
|
||||
return order_a < order_b
|
||||
if order_a >= 0 and order_b < 0:
|
||||
return true
|
||||
if order_b >= 0 and order_a < 0:
|
||||
return false
|
||||
return _sort_bag_items(a, b)
|
||||
|
||||
|
||||
func _select_bag_item(item_id: StringName) -> void:
|
||||
if _bag_drag_active or get_viewport().gui_is_dragging():
|
||||
return
|
||||
|
|
@ -3743,6 +3999,42 @@ func _on_bag_drag_started() -> void:
|
|||
_set_content_interactive(false)
|
||||
|
||||
|
||||
func _on_bag_item_dropped(item_id: StringName, slot_index: int) -> void:
|
||||
if _bag == null or item_id.is_empty() or slot_index < 0:
|
||||
return
|
||||
if _bag.move_item_to_storage_slot(item_id, slot_index):
|
||||
_select_bag_item(item_id)
|
||||
|
||||
|
||||
func _on_bag_storage_slot_pressed(slot: BagStorageSlotType) -> void:
|
||||
if (
|
||||
_controller_ownership == ControllerOwnership.STORAGE_PLACEMENT
|
||||
and slot != null
|
||||
):
|
||||
_confirm_controller_storage_placement()
|
||||
|
||||
|
||||
func _on_bag_storage_slot_focus_entered(slot: BagStorageSlotType) -> void:
|
||||
if _controller_ownership != ControllerOwnership.STORAGE_PLACEMENT:
|
||||
return
|
||||
var texture := _controller_storage_texture()
|
||||
for candidate: BagStorageSlotType in _bag_slot_nodes:
|
||||
if is_instance_valid(candidate):
|
||||
candidate.set_placement_preview(texture, candidate == slot)
|
||||
|
||||
|
||||
func _controller_storage_texture() -> Texture2D:
|
||||
var item: ItemDataType = (
|
||||
_item_catalog.get_item_by_id(_controller_storage_identity)
|
||||
if (
|
||||
_item_catalog != null
|
||||
and not _controller_storage_identity.is_empty()
|
||||
)
|
||||
else null
|
||||
)
|
||||
return item.icon if item != null else null
|
||||
|
||||
|
||||
func _on_bag_drag_finished() -> void:
|
||||
_bag_drag_active = false
|
||||
if visible and _current_section == Section.BAG:
|
||||
|
|
@ -3839,6 +4131,9 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
|||
fish_node.pressed.connect(
|
||||
_on_catch_card_pressed.bind(fish_catch.catch_id)
|
||||
)
|
||||
fish_node.focus_entered.connect(
|
||||
_on_catch_card_focused.bind(fish_catch.catch_id)
|
||||
)
|
||||
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
||||
fish_node.configure(
|
||||
fish_catch.catch_id,
|
||||
|
|
@ -4067,6 +4362,24 @@ func _on_catch_card_pressed(catch_id: StringName) -> void:
|
|||
_refresh_inventory()
|
||||
|
||||
|
||||
func _on_catch_card_focused(catch_id: StringName) -> void:
|
||||
if _inventory == null or _inventory.get_catch(catch_id) == null:
|
||||
return
|
||||
_fish_selection.focus_only(catch_id)
|
||||
for visible_catch: FishCatchType in _sorted_catches:
|
||||
var fish_node := _fish_nodes.get(
|
||||
visible_catch.catch_id
|
||||
) as CoolerFishSpriteType
|
||||
if fish_node == null:
|
||||
continue
|
||||
fish_node.set_item_state(
|
||||
_fish_selection.is_selected(visible_catch.catch_id),
|
||||
visible_catch.catch_id == catch_id,
|
||||
visible_catch.is_favorited,
|
||||
)
|
||||
_update_inventory_detail(_inventory.get_catch(catch_id))
|
||||
|
||||
|
||||
func _on_fish_field_gui_input(event: InputEvent) -> void:
|
||||
if (
|
||||
event is InputEventMouseButton
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue