diff --git a/ui/components/bubble_hotbar/bubble_hotbar_slot.gd b/ui/components/bubble_hotbar/bubble_hotbar_slot.gd new file mode 100644 index 0000000..09d2346 --- /dev/null +++ b/ui/components/bubble_hotbar/bubble_hotbar_slot.gd @@ -0,0 +1,372 @@ +class_name BubbleHotbarSlot +extends Button + +signal item_hovered(slot_index: int, item_id: StringName) +signal item_hover_ended(slot_index: int) +signal item_drag_started +signal item_drag_finished + +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 PlayerHotbarType = preload("res://inventory/player_hotbar.gd") + +const SELECTED_SCALE: float = 1.12 +const HOVER_SCALE: float = 1.025 +const MAXIMUM_SCALE: float = 1.14 +const SELECTED_LIFT: float = 4.0 +const HOVER_LIFT: float = 0.75 +const PRESENTATION_RESPONSE: float = 18.0 +const IDLE_AMPLITUDE: float = 1.0 +const IDLE_PERIOD: float = 6.0 +const DEFORMATION_PERIOD: float = 6.8 +const DEFORMATION_PHASE_MULTIPLIER: float = 1.37 +const DEFORMATION_X_AMPLITUDE: float = 0.009 +const DEFORMATION_Y_AMPLITUDE: float = 0.007 + +@export_range(0, PlayerHotbarType.SLOT_COUNT - 1, 1) var slot_index: int = 0 +@export_group("Authored Layout") +@export var desktop_size: Vector2 = Vector2(80.0, 78.0) +@export var compact_size: Vector2 = Vector2(56.0, 54.0) +@export var desktop_anchor: Vector2 = Vector2.ZERO +@export var compact_anchor: Vector2 = Vector2.ZERO +@export_group("Motion") +@export_range(0.0, TAU, 0.01) var motion_phase: float = 0.0 +@export_group("Style") +@export var profile: BubbleMenuProfile + +@onready var _item_icon: TextureRect = %ItemIcon +@onready var _slot_number_label: Label = %SlotNumberLabel +@onready var _quantity_label: Label = %QuantityLabel + +var _hotbar: PlayerHotbarType +var _bag: PlayerBagType +var _catalog: ItemCatalogType +var _drag_enabled: bool = false +var _drag_in_progress: bool = false +var _selected: bool = false +var _hovered: bool = false +var _empty: bool = true +var _selection_amount: float = 0.0 +var _hover_amount: float = 0.0 +var _base_position: Vector2 = Vector2.ZERO +var _presented_size: Vector2 = Vector2.ZERO +var _compact: bool = false +var _presentation_initialized: bool = false + + +func _ready() -> void: + focus_mode = Control.FOCUS_NONE + toggle_mode = false + pressed.connect(_select_slot) + mouse_entered.connect(_on_mouse_entered) + mouse_exited.connect(_on_mouse_exited) + resized.connect(_update_pivot) + _slot_number_label.text = str(slot_index + 1) + _update_pivot() + _apply_style() + + +func setup( + hotbar: PlayerHotbarType, + bag: PlayerBagType, + catalog: ItemCatalogType, +) -> void: + _hotbar = hotbar + _bag = bag + _catalog = catalog + _slot_number_label.text = str(slot_index + 1) + refresh() + + +func apply_layout(compact: bool) -> void: + _compact = compact + _presented_size = compact_size if compact else desktop_size + var center: Vector2 = compact_anchor if compact else desktop_anchor + custom_minimum_size = _presented_size + size = _presented_size + _base_position = center - _presented_size * 0.5 + _update_content_layout() + _update_pivot() + _apply_style() + if not _presentation_initialized: + _selection_amount = 1.0 if _selected else 0.0 + _hover_amount = 1.0 if _hovered else 0.0 + _presentation_initialized = true + + +func advance_presentation(delta: float, elapsed: float) -> void: + var response_weight: float = 1.0 - exp(-PRESENTATION_RESPONSE * delta) + _selection_amount = lerpf( + _selection_amount, + 1.0 if _selected else 0.0, + response_weight + ) + _hover_amount = lerpf( + _hover_amount, + 1.0 if _hovered else 0.0, + response_weight + ) + var idle_offset: float = sin( + elapsed / IDLE_PERIOD * TAU + motion_phase + ) * IDLE_AMPLITUDE + var lift: float = ( + _selection_amount * SELECTED_LIFT + + _hover_amount * HOVER_LIFT + ) + position = _base_position + Vector2(0.0, idle_offset - lift) + var presentation_scale: float = minf( + MAXIMUM_SCALE, + 1.0 + + _selection_amount * (SELECTED_SCALE - 1.0) + + _hover_amount * (HOVER_SCALE - 1.0) + ) + var breath: float = sin( + elapsed / DEFORMATION_PERIOD * TAU + + motion_phase * DEFORMATION_PHASE_MULTIPLIER + ) + var deformation := Vector2( + 1.0 + breath * DEFORMATION_X_AMPLITUDE, + 1.0 - breath * DEFORMATION_Y_AMPLITUDE + ) + scale = deformation * presentation_scale + + +func set_drag_enabled(enabled: bool) -> void: + _drag_enabled = enabled + mouse_filter = ( + Control.MOUSE_FILTER_STOP + if enabled + else Control.MOUSE_FILTER_IGNORE + ) + if not enabled: + _hovered = false + + +func refresh() -> void: + if _hotbar == null: + return + var item_id: StringName = _hotbar.get_item_id(slot_index) + var item: ItemDataType = ( + _catalog.get_item_by_id(item_id) + if _catalog != null and not item_id.is_empty() + else null + ) + _item_icon.texture = item.icon if item != null else null + var quantity: int = ( + _bag.get_quantity(item_id) + if _bag != null and not item_id.is_empty() + else 0 + ) + var quantity_text: String = ( + "×%d" % quantity + if item != null and item.stackable and quantity > 1 + else "" + ) + _quantity_label.text = quantity_text + _quantity_label.visible = not quantity_text.is_empty() + tooltip_text = ( + item.display_name if item != null else "empty hotbar slot" + ) + var was_selected: bool = _selected + var was_empty: bool = _empty + _selected = slot_index == _hotbar.get_selected_slot() + _empty = item == null + if was_selected != _selected or was_empty != _empty: + _apply_style() + + +func _update_content_layout() -> void: + if not is_node_ready(): + return + if _compact: + _item_icon.position = Vector2(11.0, 11.0) + _item_icon.size = Vector2(34.0, 34.0) + _slot_number_label.position = Vector2(6.0, 2.0) + _slot_number_label.size = Vector2(18.0, 18.0) + _slot_number_label.add_theme_font_size_override("font_size", 10) + _quantity_label.position = Vector2(25.0, 35.0) + _quantity_label.size = Vector2(25.0, 17.0) + _quantity_label.add_theme_font_size_override("font_size", 10) + else: + _item_icon.position = Vector2(17.0, 16.0) + _item_icon.size = Vector2(46.0, 46.0) + _slot_number_label.position = Vector2(8.0, 3.0) + _slot_number_label.size = Vector2(22.0, 20.0) + _slot_number_label.add_theme_font_size_override("font_size", 12) + _quantity_label.position = Vector2(39.0, 54.0) + _quantity_label.size = Vector2(32.0, 20.0) + _quantity_label.add_theme_font_size_override("font_size", 12) + + +func _apply_style() -> void: + if profile == null: + return + var normal_fill: Color = profile.normal_fill + normal_fill.a = 1.0 + var normal_border: Color = profile.normal_border + var normal_border_width: int = profile.normal_border_width + if _selected: + normal_border = profile.pressed_border + normal_border_width = profile.emphasized_border_width + add_theme_stylebox_override( + "normal", + _make_style(normal_fill, normal_border, normal_border_width) + ) + add_theme_stylebox_override( + "hover", + _make_style( + normal_fill, + profile.hover_border, + profile.emphasized_border_width + ) + ) + add_theme_stylebox_override( + "pressed", + _make_style( + normal_fill, + profile.pressed_border, + profile.emphasized_border_width + ) + ) + add_theme_stylebox_override( + "focus", + get_theme_stylebox("normal") + ) + add_theme_stylebox_override( + "disabled", + _make_style( + normal_fill, + profile.disabled_border, + profile.normal_border_width + ) + ) + add_theme_color_override("font_color", profile.text_color) + add_theme_color_override("font_hover_color", profile.text_hover_color) + add_theme_color_override("font_pressed_color", profile.text_pressed_color) + add_theme_color_override("font_disabled_color", profile.text_disabled_color) + + +func _make_style( + fill_color: Color, + border_color: Color, + border_width: int, +) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = fill_color + style.border_color = border_color + style.border_width_left = border_width + style.border_width_top = border_width + style.border_width_right = border_width + style.border_width_bottom = border_width + var radius: int = ceili(maxf(_presented_size.x, _presented_size.y) * 0.5) + style.corner_radius_top_left = radius + style.corner_radius_top_right = radius + style.corner_radius_bottom_right = radius + style.corner_radius_bottom_left = radius + return style + + +func _has_point(point: Vector2) -> bool: + var radius: Vector2 = size * 0.5 + if radius.x <= 0.0 or radius.y <= 0.0: + return false + var normalized: Vector2 = (point - radius) / radius + return normalized.length_squared() <= 1.0 + + +func _gui_input(event: InputEvent) -> void: + if ( + _drag_enabled + and event is InputEventMouseButton + and event.button_index == MOUSE_BUTTON_RIGHT + and event.pressed + and _hotbar != null + ): + _hotbar.clear_slot(slot_index) + accept_event() + + +func _get_drag_data(_at_position: Vector2) -> Variant: + if not _drag_enabled or _hotbar == null: + return null + var item_id: StringName = _hotbar.get_item_id(slot_index) + if item_id.is_empty(): + return null + var item: ItemDataType = _catalog.get_item_by_id(item_id) + var preview := TextureRect.new() + preview.custom_minimum_size = Vector2(44.0, 44.0) + preview.texture = item.icon if item != null else null + preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + preview.mouse_filter = Control.MOUSE_FILTER_IGNORE + set_drag_preview(preview) + _drag_in_progress = true + item_drag_started.emit() + return { + "kind": "hotbar_slot", + "slot_index": slot_index, + "item_id": String(item_id), + } + + +func _can_drop_data(_at_position: Vector2, data: Variant) -> bool: + if not _drag_enabled or typeof(data) != TYPE_DICTIONARY: + return false + var payload: Dictionary = data + var kind: String = str(payload.get("kind", "")) + if kind == "hotbar_slot": + var source_index: int = int(payload.get("slot_index", -1)) + return ( + source_index >= 0 + and source_index < PlayerHotbarType.SLOT_COUNT + and source_index != slot_index + ) + if kind != "bag_item": + return false + var item_id: StringName = StringName(str(payload.get("item_id", ""))) + if _bag == null or not _bag.owns_item(item_id) or _catalog == null: + return false + var item: ItemDataType = _catalog.get_item_by_id(item_id) + return item != null and item.is_valid() and item.hotbar_allowed + + +func _drop_data(_at_position: Vector2, data: Variant) -> void: + if not _can_drop_data(Vector2.ZERO, data) or _hotbar == null: + return + var payload: Dictionary = data + if str(payload.get("kind", "")) == "hotbar_slot": + _hotbar.swap_slots(int(payload["slot_index"]), slot_index) + else: + _hotbar.assign_item( + slot_index, + StringName(str(payload["item_id"])) + ) + + +func _select_slot() -> void: + if _hotbar != null: + _hotbar.select_slot(slot_index) + refresh() + + +func _on_mouse_entered() -> void: + _hovered = true + if _hotbar != null: + item_hovered.emit(slot_index, _hotbar.get_item_id(slot_index)) + + +func _on_mouse_exited() -> void: + _hovered = false + item_hover_ended.emit(slot_index) + + +func _update_pivot() -> void: + pivot_offset = size * 0.5 + + +func _notification(what: int) -> void: + if what == NOTIFICATION_DRAG_END and _drag_in_progress: + _drag_in_progress = false + item_drag_finished.emit() diff --git a/ui/components/bubble_hotbar/bubble_hotbar_slot.gd.uid b/ui/components/bubble_hotbar/bubble_hotbar_slot.gd.uid new file mode 100644 index 0000000..9afbb1c --- /dev/null +++ b/ui/components/bubble_hotbar/bubble_hotbar_slot.gd.uid @@ -0,0 +1 @@ +uid://bvkhp5ly74750 diff --git a/ui/components/bubble_hotbar/bubble_hotbar_slot.tscn b/ui/components/bubble_hotbar/bubble_hotbar_slot.tscn new file mode 100644 index 0000000..280f03d --- /dev/null +++ b/ui/components/bubble_hotbar/bubble_hotbar_slot.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd" id="1_slot"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="2_profile"] +[ext_resource type="Theme" path="res://ui/game_theme.tres" id="3_theme"] + +[node name="BubbleHotbarSlot" type="Button"] +custom_minimum_size = Vector2(80, 78) +focus_mode = 0 +mouse_default_cursor_shape = 2 +theme = ExtResource("3_theme") +script = ExtResource("1_slot") +profile = ExtResource("2_profile") + +[node name="ItemIcon" type="TextureRect" parent="."] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 17.0 +offset_top = 16.0 +offset_right = 63.0 +offset_bottom = 62.0 +mouse_filter = 2 +texture_filter = 1 +expand_mode = 1 +stretch_mode = 5 + +[node name="SlotNumberLabel" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 8.0 +offset_top = 3.0 +offset_right = 30.0 +offset_bottom = 23.0 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_colors/font_outline_color = Color(0.91, 0.98, 1, 0.8) +theme_override_constants/outline_size = 1 +theme_override_font_sizes/font_size = 12 +text = "1" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="QuantityLabel" type="Label" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 0 +offset_left = 39.0 +offset_top = 54.0 +offset_right = 71.0 +offset_bottom = 74.0 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_colors/font_outline_color = Color(0.91, 0.98, 1, 0.9) +theme_override_constants/outline_size = 1 +theme_override_font_sizes/font_size = 12 +horizontal_alignment = 2 +vertical_alignment = 1 diff --git a/ui/hotbar.gd b/ui/hotbar.gd index af6ade1..427c46a 100644 --- a/ui/hotbar.gd +++ b/ui/hotbar.gd @@ -4,10 +4,16 @@ extends Control const ItemCatalogType = preload("res://items/item_catalog.gd") const PlayerBagType = preload("res://inventory/player_bag.gd") const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") -const HotbarSlotType = preload("res://ui/hotbar_slot.gd") +const BubbleHotbarSlotType = preload( + "res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd" +) const FishingSpotType = preload("res://fishing/fishing_spot.gd") -@onready var _slot_row: HBoxContainer = %SlotRow +const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0) +const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0) + +@onready var _presentation_scale_root: Control = %HotbarPresentationScaleRoot +@onready var _bubble_field: Control = %BubbleField @onready var _selected_item_label: Label = %SelectedItemLabel @onready var _item_name_timer: Timer = %ItemNameTimer @@ -15,15 +21,26 @@ var _hotbar: PlayerHotbarType var _bag: PlayerBagType var _catalog: ItemCatalogType var _fishing_spot: FishingSpotType -var _slots: Array[HotbarSlotType] = [] +var _slots: Array[BubbleHotbarSlotType] = [] var _gameplay_input_enabled: bool = false var _drag_enabled: bool = false var _hovered_slot_index: int = -1 var _item_name_suppressed: bool = false +var _motion_elapsed: float = 0.0 +var _compact_layout: bool = false func _ready() -> void: _item_name_timer.timeout.connect(_on_item_name_timer_timeout) + resized.connect(_apply_layout) + _collect_slots() + _apply_layout() + + +func _process(delta: float) -> void: + _motion_elapsed += delta + for slot: BubbleHotbarSlotType in _slots: + slot.advance_presentation(delta, _motion_elapsed) func setup( @@ -44,7 +61,9 @@ func setup( _hotbar.selected_slot_changed.connect(_on_selected_slot_changed) if not _bag.contents_changed.is_connected(_refresh): _bag.contents_changed.connect(_refresh) - _build_slots() + for slot: BubbleHotbarSlotType in _slots: + slot.setup(_hotbar, _bag, _catalog) + slot.set_drag_enabled(_drag_enabled) _refresh() @@ -54,7 +73,7 @@ func set_gameplay_input_enabled(enabled: bool) -> void: func set_drag_enabled(enabled: bool) -> void: _drag_enabled = enabled - for slot: HotbarSlotType in _slots: + for slot: BubbleHotbarSlotType in _slots: slot.set_drag_enabled(enabled) if not enabled: _hovered_slot_index = -1 @@ -95,25 +114,69 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() -func _build_slots() -> void: - for child: Node in _slot_row.get_children(): - child.queue_free() +func _collect_slots() -> void: _slots.clear() - for index: int in range(PlayerHotbarType.SLOT_COUNT): - var slot := HotbarSlotType.new() - slot.toggle_mode = true - slot.setup(index, _hotbar, _bag, _catalog) - slot.set_drag_enabled(_drag_enabled) + for child: Node in _bubble_field.get_children(): + var slot := child as BubbleHotbarSlotType + if slot == null: + continue slot.item_hovered.connect(_on_slot_item_hovered) slot.item_hover_ended.connect(_on_slot_item_hover_ended) slot.item_drag_started.connect(_on_slot_drag_started) slot.item_drag_finished.connect(_on_slot_drag_finished) - _slot_row.add_child(slot) _slots.append(slot) + _slots.sort_custom( + func( + left: BubbleHotbarSlotType, + right: BubbleHotbarSlotType, + ) -> bool: + return left.slot_index < right.slot_index + ) + + +func _apply_layout() -> void: + if not is_node_ready(): + return + var parent_control := get_parent_control() + var available_size: Vector2 = ( + parent_control.size if parent_control != null else size + ) + _compact_layout = ( + available_size.y < PlayerSettings.COMPACT_DISPLAY_HEIGHT + ) + var reference_size := ( + COMPACT_REFERENCE_SIZE + if _compact_layout + else DESKTOP_REFERENCE_SIZE + ) + var presentation_scale: float = minf( + available_size.x / reference_size.x, + available_size.y / reference_size.y + ) + if _compact_layout: + presentation_scale = maxf(1.0, presentation_scale) + _presentation_scale_root.size = reference_size + _presentation_scale_root.scale = Vector2.ONE * presentation_scale + _presentation_scale_root.position = Vector2( + (available_size.x - reference_size.x * presentation_scale) * 0.5, + available_size.y - reference_size.y * presentation_scale + ) + var field_size := ( + Vector2(600.0, 82.0) + if _compact_layout + else Vector2(820.0, 104.0) + ) + _bubble_field.size = field_size + _bubble_field.position = Vector2( + (reference_size.x - field_size.x) * 0.5, + reference_size.y - 8.0 - field_size.y + ) + for slot: BubbleHotbarSlotType in _slots: + slot.apply_layout(_compact_layout) func _refresh() -> void: - for slot: HotbarSlotType in _slots: + for slot: BubbleHotbarSlotType in _slots: slot.refresh() diff --git a/ui/hotbar.tscn b/ui/hotbar.tscn index 7b73bc1..eaf704c 100644 --- a/ui/hotbar.tscn +++ b/ui/hotbar.tscn @@ -1,25 +1,24 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=4 format=3] [ext_resource type="Script" path="res://ui/hotbar.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] +[ext_resource type="PackedScene" path="res://ui/components/bubble_hotbar/bubble_hotbar_slot.tscn" id="3_slot"] [node name="Hotbar" type="Control"] unique_name_in_owner = true visible = false z_index = 20 layout_mode = 3 -anchors_preset = 12 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -offset_top = -84.0 -offset_bottom = -8.0 grow_horizontal = 2 -grow_vertical = 0 +grow_vertical = 2 mouse_filter = 2 theme = ExtResource("2_theme") script = ExtResource("1_script") -[node name="Center" type="CenterContainer" parent="."] +[node name="ResponsiveHotbarStage" type="Control" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -28,36 +27,100 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="Panel" type="PanelContainer" parent="Center"] -layout_mode = 2 -mouse_filter = 2 - -[node name="Margin" type="MarginContainer" parent="Center/Panel"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="SlotRow" type="HBoxContainer" parent="Center/Panel/Margin"] +[node name="HotbarPresentationScaleRoot" type="Control" parent="ResponsiveHotbarStage"] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 0 +offset_right = 1280.0 +offset_bottom = 720.0 mouse_filter = 2 -theme_override_constants/separation = 4 -[node name="SelectedItemLabel" type="Label" parent="."] +[node name="BubbleField" type="Control" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot"] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 230.0 +offset_top = 608.0 +offset_right = 1050.0 +offset_bottom = 712.0 +mouse_filter = 2 + +[node name="Slot1" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 0 +desktop_anchor = Vector2(58, 49) +compact_anchor = Vector2(52, 42) +motion_phase = 0.15 + +[node name="Slot2" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 1 +desktop_anchor = Vector2(146, 53) +compact_anchor = Vector2(114, 46) +motion_phase = 0.82 + +[node name="Slot3" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 2 +desktop_anchor = Vector2(234, 57) +compact_anchor = Vector2(176, 50) +motion_phase = 1.49 + +[node name="Slot4" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 3 +desktop_anchor = Vector2(322, 60) +compact_anchor = Vector2(238, 53) +motion_phase = 2.16 + +[node name="Slot5" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 4 +desktop_anchor = Vector2(410, 62) +compact_anchor = Vector2(300, 55) +motion_phase = 2.83 + +[node name="Slot6" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 5 +desktop_anchor = Vector2(498, 60) +compact_anchor = Vector2(362, 53) +motion_phase = 3.5 + +[node name="Slot7" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 6 +desktop_anchor = Vector2(586, 57) +compact_anchor = Vector2(424, 50) +motion_phase = 4.17 + +[node name="Slot8" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 7 +desktop_anchor = Vector2(674, 53) +compact_anchor = Vector2(486, 46) +motion_phase = 4.84 + +[node name="Slot9" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot/BubbleField" instance=ExtResource("3_slot")] +layout_mode = 0 +slot_index = 8 +desktop_anchor = Vector2(762, 49) +compact_anchor = Vector2(548, 42) +motion_phase = 5.51 + +[node name="SelectedItemLabel" type="Label" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot"] unique_name_in_owner = true visible = false layout_mode = 1 -anchors_preset = 10 +anchors_preset = 12 anchor_left = 0.5 +anchor_top = 1.0 anchor_right = 0.5 +anchor_bottom = 1.0 offset_left = -150.0 -offset_top = -28.0 +offset_top = -132.0 offset_right = 150.0 -offset_bottom = -8.0 +offset_bottom = -112.0 grow_horizontal = 2 +grow_vertical = 0 mouse_filter = 2 clip_text = true text_overrun_behavior = 3 diff --git a/ui/hotbar_slot.gd b/ui/hotbar_slot.gd deleted file mode 100644 index f531312..0000000 --- a/ui/hotbar_slot.gd +++ /dev/null @@ -1,187 +0,0 @@ -class_name HotbarSlot -extends Button - -signal item_hovered(slot_index: int, item_id: StringName) -signal item_hover_ended(slot_index: int) -signal item_drag_started -signal item_drag_finished - -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 PlayerHotbarType = preload("res://inventory/player_hotbar.gd") - -var slot_index: int = 0 -var _hotbar: PlayerHotbarType -var _bag: PlayerBagType -var _catalog: ItemCatalogType -var _drag_enabled: bool = false -var _drag_in_progress: bool = false -var _slot_number_label: Label -var _quantity_label: Label - - -func setup( - index: int, - hotbar: PlayerHotbarType, - bag: PlayerBagType, - catalog: ItemCatalogType, -) -> void: - slot_index = index - _hotbar = hotbar - _bag = bag - _catalog = catalog - custom_minimum_size = Vector2(50, 50) - expand_icon = true - focus_mode = Control.FOCUS_ALL - _slot_number_label = _create_corner_label(str(slot_index + 1)) - _quantity_label = _create_corner_label("", true) - pressed.connect(_select_slot) - mouse_entered.connect(_on_mouse_entered) - mouse_exited.connect(_on_mouse_exited) - refresh() - - -func set_drag_enabled(enabled: bool) -> void: - _drag_enabled = enabled - mouse_filter = ( - Control.MOUSE_FILTER_STOP - if enabled - else Control.MOUSE_FILTER_IGNORE - ) - - -func refresh() -> void: - if _hotbar == null: - return - var item_id: StringName = _hotbar.get_item_id(slot_index) - var item: ItemDataType = ( - _catalog.get_item_by_id(item_id) - if _catalog != null and not item_id.is_empty() - else null - ) - icon = item.icon if item != null else null - var quantity: int = ( - _bag.get_quantity(item_id) - if _bag != null and not item_id.is_empty() - else 0 - ) - var quantity_text: String = ( - "×%d" % quantity - if item != null and item.stackable and quantity > 1 - else "" - ) - text = "" - _quantity_label.text = quantity_text - _quantity_label.visible = not quantity_text.is_empty() - tooltip_text = ( - item.display_name if item != null else "empty hotbar slot" - ) - button_pressed = slot_index == _hotbar.get_selected_slot() - - -func _create_corner_label( - label_text: String, - bottom_right: bool = false, -) -> Label: - var label := Label.new() - label.text = label_text - label.mouse_filter = Control.MOUSE_FILTER_IGNORE - label.add_theme_font_size_override("font_size", 11) - label.add_theme_color_override("font_color", UIPalette.TEXT) - if bottom_right: - label.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) - label.position = Vector2(-22.0, -20.0) - else: - label.position = Vector2(5.0, 2.0) - add_child(label) - return label - - -func _gui_input(event: InputEvent) -> void: - if ( - _drag_enabled - and event is InputEventMouseButton - and event.button_index == MOUSE_BUTTON_RIGHT - and event.pressed - and _hotbar != null - ): - _hotbar.clear_slot(slot_index) - accept_event() - - -func _get_drag_data(_at_position: Vector2) -> Variant: - if not _drag_enabled or _hotbar == null: - return null - var item_id: StringName = _hotbar.get_item_id(slot_index) - if item_id.is_empty(): - return null - var item: ItemDataType = _catalog.get_item_by_id(item_id) - var preview := TextureRect.new() - preview.custom_minimum_size = Vector2(44, 44) - preview.texture = item.icon if item != null else null - preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - preview.mouse_filter = Control.MOUSE_FILTER_IGNORE - set_drag_preview(preview) - _drag_in_progress = true - item_drag_started.emit() - return { - "kind": "hotbar_slot", - "slot_index": slot_index, - "item_id": String(item_id), - } - - -func _can_drop_data(_at_position: Vector2, data: Variant) -> bool: - if not _drag_enabled or typeof(data) != TYPE_DICTIONARY: - return false - var payload: Dictionary = data - var kind: String = str(payload.get("kind", "")) - if kind == "hotbar_slot": - var source_index: int = int(payload.get("slot_index", -1)) - return ( - source_index >= 0 - and source_index < PlayerHotbarType.SLOT_COUNT - and source_index != slot_index - ) - if kind != "bag_item": - return false - var item_id: StringName = StringName(str(payload.get("item_id", ""))) - if _bag == null or not _bag.owns_item(item_id) or _catalog == null: - return false - var item: ItemDataType = _catalog.get_item_by_id(item_id) - return item != null and item.is_valid() and item.hotbar_allowed - - -func _drop_data(_at_position: Vector2, data: Variant) -> void: - if not _can_drop_data(Vector2.ZERO, data) or _hotbar == null: - return - var payload: Dictionary = data - if str(payload.get("kind", "")) == "hotbar_slot": - _hotbar.swap_slots(int(payload["slot_index"]), slot_index) - else: - _hotbar.assign_item( - slot_index, - StringName(str(payload["item_id"])) - ) - - -func _select_slot() -> void: - if _hotbar != null: - _hotbar.select_slot(slot_index) - - -func _on_mouse_entered() -> void: - if _hotbar != null: - item_hovered.emit(slot_index, _hotbar.get_item_id(slot_index)) - - -func _on_mouse_exited() -> void: - item_hover_ended.emit(slot_index) - - -func _notification(what: int) -> void: - if what == NOTIFICATION_DRAG_END and _drag_in_progress: - _drag_in_progress = false - item_drag_finished.emit() diff --git a/ui/hotbar_slot.gd.uid b/ui/hotbar_slot.gd.uid deleted file mode 100644 index dd28680..0000000 --- a/ui/hotbar_slot.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://swcob5q48f8e