Migrate hotbar to bubble UI
This commit is contained in:
parent
48a3ab03ea
commit
22f7119b72
7 changed files with 596 additions and 228 deletions
372
ui/components/bubble_hotbar/bubble_hotbar_slot.gd
Normal file
372
ui/components/bubble_hotbar/bubble_hotbar_slot.gd
Normal file
|
|
@ -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()
|
||||
1
ui/components/bubble_hotbar/bubble_hotbar_slot.gd.uid
Normal file
1
ui/components/bubble_hotbar/bubble_hotbar_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bvkhp5ly74750
|
||||
57
ui/components/bubble_hotbar/bubble_hotbar_slot.tscn
Normal file
57
ui/components/bubble_hotbar/bubble_hotbar_slot.tscn
Normal file
|
|
@ -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
|
||||
93
ui/hotbar.gd
93
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()
|
||||
|
||||
|
||||
|
|
|
|||
113
ui/hotbar.tscn
113
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://swcob5q48f8e
|
||||
Loading…
Add table
Add a link
Reference in a new issue