class_name PlayerStorage extends Control const INPUT_OWNER: StringName = &"player_storage" signal menu_visibility_changed(is_open: bool) signal decorate_requested var _player: Player var _fishing_spot: FishingSpot var _interaction: PlayerStorageInteraction var _layout: PlayerInventoryLayout var _inventory_grid: GeneralInventoryGrid var _storage_grid: GeneralInventoryGrid var _inventory_count: Label var _storage_count: Label var _feedback: Label var _title: Label var _decorate_button: Button var _home_access: bool = false var _prior_movement_enabled: bool = true var _prior_camera_enabled: bool = true var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE func _ready() -> void: UtilityPageStyle.apply_page(self) _build_ui() hide() func setup( player: Player, fishing_spot: FishingSpot, interaction: PlayerStorageInteraction, layout: PlayerInventoryLayout, bag: PlayerBag, fish_inventory: FishInventory, hotbar: PlayerHotbar, item_catalog: ItemCatalog, ) -> void: _player = player _fishing_spot = fishing_spot _interaction = interaction _layout = layout _inventory_grid.setup( layout, bag, fish_inventory, hotbar, item_catalog, PlayerInventoryLayout.InventoryContainer.INVENTORY, ) _storage_grid.setup( layout, bag, fish_inventory, hotbar, item_catalog, PlayerInventoryLayout.InventoryContainer.STORAGE, ) _inventory_grid.entry_selected.connect( _on_entry_selected.bind( PlayerInventoryLayout.InventoryContainer.STORAGE ) ) _storage_grid.entry_selected.connect( _on_entry_selected.bind( PlayerInventoryLayout.InventoryContainer.INVENTORY ) ) if _layout != null and not _layout.layout_changed.is_connected(_refresh): _layout.layout_changed.connect(_refresh) _refresh() func set_world_interaction(interaction: PlayerStorageInteraction) -> void: _interaction = interaction func open_storage() -> bool: return _open_storage(false) func open_storage_from_home(allow_decorating: bool = true) -> bool: return _open_storage(true, allow_decorating) func _open_storage( home_access: bool, allow_decorating: bool = false, ) -> bool: if ( visible or _player == null or _fishing_spot == null or not _fishing_spot.can_open_fishing_shop() or ( not home_access and ( _interaction == null or not _interaction.is_local_player_in_range() ) ) ): return false _home_access = home_access _title.text = "RV storage" if home_access else "private storage" _decorate_button.visible = home_access and allow_decorating _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_enabled = _player.is_camera_input_enabled() _prior_mouse_mode = Input.mouse_mode _player.set_movement_enabled(false) _player.set_camera_input_enabled(false) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _feedback.text = "select an item to move it · drag to choose a slot" _refresh() show() call_deferred("_focus_first_slot") menu_visibility_changed.emit(true) return true func close_storage(restore_controls: bool = true) -> void: if not visible: return var viewport := get_viewport() if viewport != null: viewport.gui_release_focus() hide() if _fishing_spot != null: _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) if restore_controls and _player != null: _player.set_movement_enabled(_prior_movement_enabled) _player.set_camera_input_enabled(_prior_camera_enabled) Input.mouse_mode = _prior_mouse_mode _home_access = false menu_visibility_changed.emit(false) func close_for_range_exit() -> void: if _home_access: return close_storage() func close_for_water_recovery() -> void: close_storage(false) func close_for_session_end() -> void: close_storage(false) func consume_escape() -> bool: if not visible: return false close_storage() return true func _unhandled_input(event: InputEvent) -> void: if not visible or not event.is_action_pressed("ui_cancel"): return close_storage() get_viewport().set_input_as_handled() func _on_entry_selected( kind: int, identity: StringName, target_container: int, ) -> void: if kind < 0 or identity.is_empty() or _layout == null: return if _layout.move_entry_to_first_free(kind, identity, target_container): _feedback.text = ( "moved to storage" if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE else "moved to inventory" ) else: _feedback.text = ( "storage is full" if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE else "inventory is full" ) func _refresh() -> void: if _layout == null: return _inventory_grid.refresh() _storage_grid.refresh() _inventory_count.text = "%d / %d" % [ _layout.get_inventory_count(), _layout.get_inventory_capacity(), ] _storage_count.text = "%d / %d" % [ _layout.get_storage_count(), _layout.get_storage_capacity(), ] call_deferred("_connect_grid_focus") func _focus_first_slot() -> void: var slot := _inventory_grid.get_first_occupied_slot() if slot == null: slot = _storage_grid.get_first_occupied_slot() if slot != null: slot.grab_focus() func _connect_grid_focus() -> void: var inventory_slots := _inventory_grid.get_slots() var storage_slots := _storage_grid.get_slots() if inventory_slots.is_empty() or storage_slots.is_empty(): return var row_count := mini( ceili( float(inventory_slots.size()) / float(PlayerInventoryLayout.INVENTORY_COLUMNS) ), ceili( float(storage_slots.size()) / float(PlayerInventoryLayout.INVENTORY_COLUMNS) ), ) for row: int in row_count: var inventory_index := mini( row * PlayerInventoryLayout.INVENTORY_COLUMNS + PlayerInventoryLayout.INVENTORY_COLUMNS - 1, inventory_slots.size() - 1, ) var storage_index := mini( row * PlayerInventoryLayout.INVENTORY_COLUMNS, storage_slots.size() - 1, ) inventory_slots[inventory_index].focus_neighbor_right = ( inventory_slots[inventory_index].get_path_to(storage_slots[storage_index]) ) storage_slots[storage_index].focus_neighbor_left = ( storage_slots[storage_index].get_path_to(inventory_slots[inventory_index]) ) func _build_ui() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) z_index = 82 mouse_filter = Control.MOUSE_FILTER_STOP var blocker := ColorRect.new() blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) blocker.color = Color(0.02, 0.08, 0.10, 0.55) blocker.mouse_filter = Control.MOUSE_FILTER_STOP add_child(blocker) var panel := PanelContainer.new() panel.set_anchors_preset(Control.PRESET_CENTER) panel.position = Vector2(-580.0, -310.0) panel.size = Vector2(1160.0, 620.0) panel.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_DEEP, 22), ) add_child(panel) var outer := MarginContainer.new() for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]: outer.add_theme_constant_override(side, 20) panel.add_child(outer) var layout := VBoxContainer.new() layout.add_theme_constant_override("separation", 12) outer.add_child(layout) var header := HBoxContainer.new() layout.add_child(header) _title = Label.new() _title.text = "private storage" _title.add_theme_font_size_override("font_size", 32) _title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY) _title.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(_title) _decorate_button = Button.new() _decorate_button.text = "decorate RV" _decorate_button.tooltip_text = "Place decor inside your RV" _decorate_button.custom_minimum_size = Vector2(160.0, 48.0) UtilityPageStyle.apply_ocean_button(_decorate_button) _decorate_button.hide() _decorate_button.pressed.connect(_request_decorating) header.add_child(_decorate_button) var close := Button.new() close.text = "×" close.tooltip_text = "Close storage" close.custom_minimum_size = Vector2(48.0, 48.0) close.flat = true close.add_theme_font_size_override("font_size", 30) close.pressed.connect(close_storage) header.add_child(close) var columns := HBoxContainer.new() columns.size_flags_vertical = Control.SIZE_EXPAND_FILL columns.add_theme_constant_override("separation", 18) layout.add_child(columns) var inventory_column := _build_column(columns, "inventory") _inventory_count = inventory_column["count"] _inventory_grid = GeneralInventoryGrid.new() (inventory_column["scroll"] as ScrollContainer).add_child(_inventory_grid) var storage_column := _build_column(columns, "storage") _storage_count = storage_column["count"] _storage_grid = GeneralInventoryGrid.new() (storage_column["scroll"] as ScrollContainer).add_child(_storage_grid) _feedback = Label.new() _feedback.text = "select an item to move it · drag to choose a slot" _feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _feedback.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) layout.add_child(_feedback) func _request_decorating() -> void: if not _home_access: return close_storage() decorate_requested.emit() func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(550.0, 0.0) panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL panel.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_MID, 16), ) parent.add_child(panel) var margin := MarginContainer.new() for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]: margin.add_theme_constant_override(side, 12) panel.add_child(margin) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 8) margin.add_child(column) var header := HBoxContainer.new() column.add_child(header) var title := Label.new() title.text = title_text title.add_theme_font_size_override("font_size", 22) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(title) var count := Label.new() count.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT count.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) header.add_child(count) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED column.add_child(scroll) return {"count": count, "scroll": scroll}