2026-07-25 20:12:29 -04:00
|
|
|
class_name HotbarUI
|
|
|
|
|
extends Control
|
|
|
|
|
|
2026-07-31 13:14:55 -04:00
|
|
|
signal presentation_transition_finished(is_visible: bool)
|
|
|
|
|
|
2026-07-25 20:12:29 -04:00
|
|
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
|
|
|
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|
|
|
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
2026-08-01 17:12:29 -04:00
|
|
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
|
|
|
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
2026-08-02 17:44:11 -04:00
|
|
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
2026-07-28 01:06:11 -04:00
|
|
|
const BubbleHotbarSlotType = preload(
|
|
|
|
|
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
|
|
|
|
)
|
2026-07-25 20:12:29 -04:00
|
|
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
|
|
|
|
|
2026-07-28 01:06:11 -04:00
|
|
|
const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
|
|
|
|
|
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
|
2026-07-29 10:53:46 -04:00
|
|
|
const HOTBAR_PRESENTATION_SCALE: float = 0.60
|
|
|
|
|
const HOTBAR_CANONICAL_POSITION := Vector2(256.0, 288.0)
|
2026-07-30 16:07:36 -04:00
|
|
|
const HOTBAR_MENU_POSITION := Vector2(80.0, 198.0)
|
2026-07-31 13:14:55 -04:00
|
|
|
const HOTBAR_GAMEPLAY_Z_INDEX: int = 35
|
|
|
|
|
# PlayerMenu is z=30 and its authored inventory panels are relative z=50.
|
|
|
|
|
const HOTBAR_MENU_Z_INDEX: int = 90
|
2026-07-28 01:06:11 -04:00
|
|
|
|
|
|
|
|
@onready var _presentation_scale_root: Control = %HotbarPresentationScaleRoot
|
|
|
|
|
@onready var _bubble_field: Control = %BubbleField
|
2026-07-25 20:12:29 -04:00
|
|
|
@onready var _selected_item_label: Label = %SelectedItemLabel
|
|
|
|
|
@onready var _item_name_timer: Timer = %ItemNameTimer
|
|
|
|
|
|
|
|
|
|
var _hotbar: PlayerHotbarType
|
|
|
|
|
var _bag: PlayerBagType
|
|
|
|
|
var _catalog: ItemCatalogType
|
2026-08-01 17:12:29 -04:00
|
|
|
var _fish_inventory: FishInventoryType
|
2026-07-25 20:12:29 -04:00
|
|
|
var _fishing_spot: FishingSpotType
|
2026-07-28 01:06:11 -04:00
|
|
|
var _slots: Array[BubbleHotbarSlotType] = []
|
2026-07-25 20:12:29 -04:00
|
|
|
var _gameplay_input_enabled: bool = false
|
|
|
|
|
var _drag_enabled: bool = false
|
|
|
|
|
var _hovered_slot_index: int = -1
|
|
|
|
|
var _item_name_suppressed: bool = false
|
2026-07-28 01:06:11 -04:00
|
|
|
var _motion_elapsed: float = 0.0
|
|
|
|
|
var _compact_layout: bool = false
|
2026-07-30 16:07:36 -04:00
|
|
|
var _player_menu_context: bool = false
|
|
|
|
|
var _visibility_tween: Tween
|
|
|
|
|
var _visibility_generation: int = 0
|
2026-07-25 20:12:29 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
_item_name_timer.timeout.connect(_on_item_name_timer_timeout)
|
2026-07-28 01:06:11 -04:00
|
|
|
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)
|
2026-07-25 20:12:29 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func setup(
|
|
|
|
|
hotbar: PlayerHotbarType,
|
|
|
|
|
bag: PlayerBagType,
|
|
|
|
|
catalog: ItemCatalogType,
|
|
|
|
|
fishing_spot: FishingSpotType,
|
2026-08-01 17:12:29 -04:00
|
|
|
fish_inventory: FishInventoryType,
|
2026-07-25 20:12:29 -04:00
|
|
|
) -> void:
|
|
|
|
|
_hotbar = hotbar
|
|
|
|
|
_bag = bag
|
|
|
|
|
_catalog = catalog
|
|
|
|
|
_fishing_spot = fishing_spot
|
2026-08-01 17:12:29 -04:00
|
|
|
_fish_inventory = fish_inventory
|
2026-07-25 20:12:29 -04:00
|
|
|
if not _hotbar.slots_changed.is_connected(_refresh):
|
|
|
|
|
_hotbar.slots_changed.connect(_refresh)
|
|
|
|
|
if not _hotbar.selected_slot_changed.is_connected(
|
|
|
|
|
_on_selected_slot_changed
|
|
|
|
|
):
|
|
|
|
|
_hotbar.selected_slot_changed.connect(_on_selected_slot_changed)
|
|
|
|
|
if not _bag.contents_changed.is_connected(_refresh):
|
|
|
|
|
_bag.contents_changed.connect(_refresh)
|
2026-08-01 17:12:29 -04:00
|
|
|
if not _fish_inventory.catches_changed.is_connected(_refresh):
|
|
|
|
|
_fish_inventory.catches_changed.connect(_refresh)
|
2026-07-28 01:06:11 -04:00
|
|
|
for slot: BubbleHotbarSlotType in _slots:
|
2026-08-01 17:12:29 -04:00
|
|
|
slot.setup(_hotbar, _bag, _catalog, _fish_inventory)
|
2026-07-28 01:06:11 -04:00
|
|
|
slot.set_drag_enabled(_drag_enabled)
|
2026-07-25 20:12:29 -04:00
|
|
|
_refresh()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func set_gameplay_input_enabled(enabled: bool) -> void:
|
|
|
|
|
_gameplay_input_enabled = enabled
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func set_drag_enabled(enabled: bool) -> void:
|
|
|
|
|
_drag_enabled = enabled
|
2026-07-28 01:06:11 -04:00
|
|
|
for slot: BubbleHotbarSlotType in _slots:
|
2026-07-25 20:12:29 -04:00
|
|
|
slot.set_drag_enabled(enabled)
|
|
|
|
|
if not enabled:
|
|
|
|
|
_hovered_slot_index = -1
|
|
|
|
|
_hide_item_name()
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 16:07:36 -04:00
|
|
|
func set_player_menu_context(enabled: bool) -> void:
|
|
|
|
|
if _player_menu_context == enabled:
|
|
|
|
|
return
|
|
|
|
|
_player_menu_context = enabled
|
2026-07-31 13:14:55 -04:00
|
|
|
z_index = HOTBAR_MENU_Z_INDEX if enabled else HOTBAR_GAMEPLAY_Z_INDEX
|
2026-07-30 16:07:36 -04:00
|
|
|
_presentation_scale_root.position = (
|
|
|
|
|
HOTBAR_MENU_POSITION if enabled else HOTBAR_CANONICAL_POSITION
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 13:14:55 -04:00
|
|
|
func set_presentation_visible(
|
|
|
|
|
should_show: bool,
|
|
|
|
|
animate: bool = true,
|
|
|
|
|
transition_duration: float = -1.0,
|
|
|
|
|
) -> void:
|
2026-07-30 16:07:36 -04:00
|
|
|
_visibility_generation += 1
|
|
|
|
|
var generation: int = _visibility_generation
|
|
|
|
|
if _visibility_tween != null:
|
|
|
|
|
_visibility_tween.kill()
|
|
|
|
|
_visibility_tween = null
|
|
|
|
|
if should_show:
|
2026-07-31 13:14:55 -04:00
|
|
|
var was_visible: bool = visible
|
2026-07-30 16:07:36 -04:00
|
|
|
visible = true
|
|
|
|
|
if not animate:
|
|
|
|
|
modulate.a = 1.0
|
2026-07-31 13:14:55 -04:00
|
|
|
presentation_transition_finished.emit(true)
|
2026-07-30 16:07:36 -04:00
|
|
|
return
|
2026-07-31 13:14:55 -04:00
|
|
|
if not was_visible:
|
|
|
|
|
modulate.a = 0.0
|
|
|
|
|
var duration: float = (
|
|
|
|
|
transition_duration
|
|
|
|
|
if transition_duration >= 0.0
|
|
|
|
|
else UIMotion.UTILITY_ENTER_DURATION
|
|
|
|
|
)
|
2026-07-30 16:07:36 -04:00
|
|
|
_visibility_tween = create_tween()
|
|
|
|
|
_visibility_tween.tween_property(
|
|
|
|
|
self,
|
|
|
|
|
"modulate:a",
|
|
|
|
|
1.0,
|
2026-07-31 13:14:55 -04:00
|
|
|
duration,
|
|
|
|
|
)
|
|
|
|
|
_visibility_tween.finished.connect(
|
|
|
|
|
func() -> void:
|
|
|
|
|
if generation != _visibility_generation:
|
|
|
|
|
return
|
|
|
|
|
_visibility_tween = null
|
|
|
|
|
presentation_transition_finished.emit(true)
|
2026-07-30 16:07:36 -04:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
if not visible:
|
|
|
|
|
return
|
|
|
|
|
if not animate:
|
|
|
|
|
visible = false
|
|
|
|
|
modulate.a = 1.0
|
2026-07-31 13:14:55 -04:00
|
|
|
presentation_transition_finished.emit(false)
|
2026-07-30 16:07:36 -04:00
|
|
|
return
|
2026-07-31 13:14:55 -04:00
|
|
|
var duration: float = (
|
|
|
|
|
transition_duration
|
|
|
|
|
if transition_duration >= 0.0
|
|
|
|
|
else UIMotion.UTILITY_EXIT_DURATION
|
|
|
|
|
)
|
2026-07-30 16:07:36 -04:00
|
|
|
_visibility_tween = create_tween()
|
|
|
|
|
_visibility_tween.tween_property(
|
|
|
|
|
self,
|
|
|
|
|
"modulate:a",
|
|
|
|
|
0.0,
|
2026-07-31 13:14:55 -04:00
|
|
|
duration,
|
2026-07-30 16:07:36 -04:00
|
|
|
)
|
|
|
|
|
_visibility_tween.finished.connect(
|
|
|
|
|
func() -> void:
|
|
|
|
|
if generation != _visibility_generation:
|
|
|
|
|
return
|
|
|
|
|
visible = false
|
|
|
|
|
modulate.a = 1.0
|
|
|
|
|
_visibility_tween = null
|
2026-07-31 13:14:55 -04:00
|
|
|
presentation_transition_finished.emit(false)
|
2026-07-30 16:07:36 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 20:12:29 -04:00
|
|
|
func set_item_name_suppressed(suppressed: bool) -> void:
|
|
|
|
|
_item_name_suppressed = suppressed
|
|
|
|
|
if suppressed:
|
|
|
|
|
_hovered_slot_index = -1
|
|
|
|
|
_hide_item_name()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
|
|
|
if (
|
|
|
|
|
not _gameplay_input_enabled
|
|
|
|
|
or _hotbar == null
|
|
|
|
|
or _fishing_spot == null
|
|
|
|
|
or not _fishing_spot.can_change_hotbar_selection()
|
|
|
|
|
):
|
|
|
|
|
return
|
2026-08-03 02:14:38 -04:00
|
|
|
if event.is_action_pressed("hotbar_previous"):
|
|
|
|
|
_hotbar.cycle_selection(-1)
|
|
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
return
|
|
|
|
|
if event.is_action_pressed("hotbar_next"):
|
|
|
|
|
_hotbar.cycle_selection(1)
|
|
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
return
|
2026-07-25 20:12:29 -04:00
|
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
|
|
|
for index: int in range(PlayerHotbarType.SLOT_COUNT):
|
|
|
|
|
if event.is_action_pressed("hotbar_%d" % (index + 1)):
|
|
|
|
|
_hotbar.select_slot(index)
|
|
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
return
|
|
|
|
|
if (
|
|
|
|
|
event is InputEventMouseButton
|
|
|
|
|
and event.pressed
|
|
|
|
|
and not event.shift_pressed
|
|
|
|
|
):
|
|
|
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
|
|
|
|
_hotbar.cycle_selection(-1)
|
|
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
|
|
|
|
_hotbar.cycle_selection(1)
|
|
|
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 01:06:11 -04:00
|
|
|
func _collect_slots() -> void:
|
2026-07-25 20:12:29 -04:00
|
|
|
_slots.clear()
|
2026-07-28 01:06:11 -04:00
|
|
|
for child: Node in _bubble_field.get_children():
|
|
|
|
|
var slot := child as BubbleHotbarSlotType
|
|
|
|
|
if slot == null:
|
|
|
|
|
continue
|
2026-07-25 20:12:29 -04:00
|
|
|
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)
|
|
|
|
|
_slots.append(slot)
|
2026-07-28 01:06:11 -04:00
|
|
|
_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
|
2026-07-29 10:53:46 -04:00
|
|
|
_compact_layout = false
|
|
|
|
|
var reference_size := DESKTOP_REFERENCE_SIZE
|
2026-07-28 01:06:11 -04:00
|
|
|
_presentation_scale_root.size = reference_size
|
2026-07-29 10:53:46 -04:00
|
|
|
_presentation_scale_root.scale = (
|
|
|
|
|
Vector2.ONE * HOTBAR_PRESENTATION_SCALE
|
2026-07-28 01:06:11 -04:00
|
|
|
)
|
2026-07-30 16:07:36 -04:00
|
|
|
_presentation_scale_root.position = (
|
|
|
|
|
HOTBAR_MENU_POSITION
|
|
|
|
|
if _player_menu_context
|
|
|
|
|
else HOTBAR_CANONICAL_POSITION
|
|
|
|
|
)
|
2026-07-28 01:06:11 -04:00
|
|
|
var field_size := (
|
2026-07-28 23:43:03 -04:00
|
|
|
Vector2(580.0, 82.0)
|
2026-07-28 01:06:11 -04:00
|
|
|
if _compact_layout
|
2026-07-28 23:43:03 -04:00
|
|
|
else Vector2(790.0, 104.0)
|
2026-07-28 01:06:11 -04:00
|
|
|
)
|
|
|
|
|
_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)
|
2026-07-25 20:12:29 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _refresh() -> void:
|
2026-07-28 01:06:11 -04:00
|
|
|
for slot: BubbleHotbarSlotType in _slots:
|
2026-07-25 20:12:29 -04:00
|
|
|
slot.refresh()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_selected_slot_changed(
|
|
|
|
|
_slot_index: int,
|
|
|
|
|
_item_id: StringName,
|
|
|
|
|
) -> void:
|
|
|
|
|
_refresh()
|
|
|
|
|
if _hovered_slot_index < 0:
|
|
|
|
|
_show_selected_item_briefly()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_slot_item_hovered(
|
|
|
|
|
slot_index: int,
|
|
|
|
|
item_id: StringName,
|
|
|
|
|
) -> void:
|
|
|
|
|
if _item_name_suppressed:
|
|
|
|
|
return
|
|
|
|
|
_hovered_slot_index = slot_index
|
|
|
|
|
_item_name_timer.stop()
|
2026-08-01 17:12:29 -04:00
|
|
|
_show_assignment_name(slot_index, item_id)
|
2026-07-25 20:12:29 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_slot_item_hover_ended(slot_index: int) -> void:
|
|
|
|
|
if slot_index != _hovered_slot_index:
|
|
|
|
|
return
|
|
|
|
|
_hovered_slot_index = -1
|
|
|
|
|
_show_selected_item_briefly()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_slot_drag_started() -> void:
|
|
|
|
|
_hovered_slot_index = -1
|
|
|
|
|
_hide_item_name()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_slot_drag_finished() -> void:
|
|
|
|
|
_show_selected_item_briefly()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _show_selected_item_briefly() -> void:
|
|
|
|
|
if _item_name_suppressed or _hotbar == null:
|
|
|
|
|
_hide_item_name()
|
|
|
|
|
return
|
2026-08-01 17:12:29 -04:00
|
|
|
var selected_slot: int = _hotbar.get_selected_slot()
|
|
|
|
|
var identity: StringName = _hotbar.get_selected_item_id()
|
|
|
|
|
if identity.is_empty():
|
|
|
|
|
identity = _hotbar.get_selected_fish_catch_id()
|
|
|
|
|
_show_assignment_name(selected_slot, identity)
|
2026-07-25 20:12:29 -04:00
|
|
|
if _selected_item_label.visible:
|
|
|
|
|
_item_name_timer.start()
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 17:12:29 -04:00
|
|
|
func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
|
|
|
|
if identity.is_empty() or _hotbar == null:
|
2026-07-25 20:12:29 -04:00
|
|
|
_hide_item_name()
|
|
|
|
|
return
|
2026-08-01 17:12:29 -04:00
|
|
|
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
|
|
|
|
if not catch_id.is_empty() and _fish_inventory != null:
|
|
|
|
|
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
|
|
|
|
if fish_catch != null:
|
2026-08-02 17:44:11 -04:00
|
|
|
_selected_item_label.text = FishQualityType.qualified_name(
|
|
|
|
|
fish_catch.fish.display_name,
|
|
|
|
|
fish_catch.quality,
|
|
|
|
|
)
|
2026-08-01 17:12:29 -04:00
|
|
|
_selected_item_label.visible = true
|
|
|
|
|
return
|
|
|
|
|
var item = (
|
|
|
|
|
_catalog.get_item_by_id(identity)
|
|
|
|
|
if _catalog != null
|
|
|
|
|
else null
|
|
|
|
|
)
|
2026-07-25 20:12:29 -04:00
|
|
|
if item == null:
|
|
|
|
|
_hide_item_name()
|
|
|
|
|
return
|
|
|
|
|
_selected_item_label.text = item.display_name
|
|
|
|
|
_selected_item_label.visible = true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _hide_item_name() -> void:
|
|
|
|
|
_item_name_timer.stop()
|
|
|
|
|
_selected_item_label.text = ""
|
|
|
|
|
_selected_item_label.visible = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_item_name_timer_timeout() -> void:
|
|
|
|
|
if _hovered_slot_index < 0:
|
|
|
|
|
_hide_item_name()
|