netfishing/ui/hotbar.gd

497 lines
14 KiB
GDScript3
Raw Permalink Normal View History

class_name HotbarUI
extends Control
signal presentation_transition_finished(is_visible: bool)
const ItemCatalogType = preload("res://items/item_catalog.gd")
2026-08-08 16:20:40 -04:00
const ItemDataType = preload("res://items/item_data.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"
)
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)
const HOTBAR_PRESENTATION_SCALE: float = 0.80
const HOTBAR_CANONICAL_POSITION := Vector2.ZERO
const HOTBAR_MENU_POSITION := Vector2(-136.0, -90.0)
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
@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
var _fishing_spot: FishingSpotType
2026-07-28 01:06:11 -04:00
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
2026-07-28 01:06:11 -04:00
var _motion_elapsed: float = 0.0
var _compact_layout: bool = false
var _player_menu_context: bool = false
var _controller_placement_active: bool = false
2026-08-08 16:20:40 -04:00
var _controller_management_active: bool = false
var _controller_placement_kind: PlayerHotbarType.AssignmentKind = (
PlayerHotbarType.AssignmentKind.EMPTY
)
var _controller_placement_identity: StringName
var _controller_placement_texture: Texture2D
var _visibility_tween: Tween
var _visibility_generation: int = 0
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)
func setup(
hotbar: PlayerHotbarType,
bag: PlayerBagType,
catalog: ItemCatalogType,
fishing_spot: FishingSpotType,
2026-08-01 17:12:29 -04:00
fish_inventory: FishInventoryType,
) -> void:
_hotbar = hotbar
_bag = bag
_catalog = catalog
_fishing_spot = fishing_spot
2026-08-01 17:12:29 -04:00
_fish_inventory = fish_inventory
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)
_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:
slot.set_drag_enabled(enabled)
if not enabled:
_hovered_slot_index = -1
_hide_item_name()
func begin_controller_placement(
assignment_kind: PlayerHotbarType.AssignmentKind,
identity: StringName,
initial_slot: int,
) -> void:
if _hotbar == null or identity.is_empty():
return
_controller_placement_active = true
_controller_placement_kind = assignment_kind
_controller_placement_identity = identity
_controller_placement_texture = _resolve_controller_placement_texture()
var slot_count: int = _slots.size()
for index: int in slot_count:
var slot: BubbleHotbarSlotType = _slots[index]
slot.focus_mode = Control.FOCUS_ALL
slot.focus_neighbor_left = slot.get_path_to(
_slots[wrapi(index - 1, 0, slot_count)]
)
slot.focus_neighbor_right = slot.get_path_to(
_slots[wrapi(index + 1, 0, slot_count)]
)
slot.focus_neighbor_top = slot.get_path_to(slot)
slot.focus_neighbor_bottom = slot.get_path_to(slot)
var target_index: int = clampi(initial_slot, 0, slot_count - 1)
_hotbar.select_slot(target_index)
_refresh_controller_placement_preview()
_slots[target_index].call_deferred("grab_focus")
func end_controller_placement() -> void:
if not _controller_placement_active:
return
_controller_placement_active = false
_controller_placement_kind = PlayerHotbarType.AssignmentKind.EMPTY
_controller_placement_identity = StringName()
_controller_placement_texture = null
for slot: BubbleHotbarSlotType in _slots:
slot.focus_mode = Control.FOCUS_NONE
slot.set_controller_placement_preview(false, null)
_show_selected_item_briefly()
2026-08-08 16:20:40 -04:00
func begin_controller_management(initial_slot: int) -> void:
if _hotbar == null or _slots.is_empty():
return
_controller_management_active = true
var slot_count: int = _slots.size()
for index: int in slot_count:
var slot: BubbleHotbarSlotType = _slots[index]
slot.focus_mode = Control.FOCUS_ALL
slot.focus_neighbor_left = slot.get_path_to(
_slots[wrapi(index - 1, 0, slot_count)]
)
slot.focus_neighbor_right = slot.get_path_to(
_slots[wrapi(index + 1, 0, slot_count)]
)
slot.focus_neighbor_top = slot.get_path_to(slot)
slot.focus_neighbor_bottom = slot.get_path_to(slot)
var target_index: int = clampi(initial_slot, 0, slot_count - 1)
_hotbar.select_slot(target_index)
_slots[target_index].call_deferred("grab_focus")
func end_controller_management() -> void:
if not _controller_management_active:
return
_controller_management_active = false
for slot: BubbleHotbarSlotType in _slots:
slot.focus_mode = Control.FOCUS_NONE
_show_selected_item_briefly()
func _resolve_controller_placement_texture() -> Texture2D:
if (
_controller_placement_kind == PlayerHotbarType.AssignmentKind.FISH
and _fish_inventory != null
):
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(
_controller_placement_identity
)
return fish_catch.fish.display_texture if fish_catch != null else null
if (
_controller_placement_kind == PlayerHotbarType.AssignmentKind.ITEM
and _catalog != null
):
var item: ItemDataType = _catalog.get_item_by_id(
_controller_placement_identity
)
return item.icon if item != null else null
return null
func _refresh_controller_placement_preview() -> void:
if not _controller_placement_active or _hotbar == null:
return
var target_index: int = _hotbar.get_selected_slot()
for slot: BubbleHotbarSlotType in _slots:
slot.set_controller_placement_preview(
slot.slot_index == target_index,
_controller_placement_texture,
)
func set_player_menu_context(enabled: bool) -> void:
if _player_menu_context == enabled:
return
_player_menu_context = enabled
z_index = HOTBAR_MENU_Z_INDEX if enabled else HOTBAR_GAMEPLAY_Z_INDEX
_presentation_scale_root.position = _presentation_position()
func set_presentation_visible(
should_show: bool,
animate: bool = true,
transition_duration: float = -1.0,
) -> void:
_visibility_generation += 1
var generation: int = _visibility_generation
if _visibility_tween != null:
_visibility_tween.kill()
_visibility_tween = null
if should_show:
var was_visible: bool = visible
visible = true
if not animate:
modulate.a = 1.0
presentation_transition_finished.emit(true)
return
if not was_visible:
modulate.a = 0.0
var duration: float = (
transition_duration
if transition_duration >= 0.0
else UIMotion.UTILITY_ENTER_DURATION
)
_visibility_tween = create_tween()
_visibility_tween.tween_property(
self,
"modulate:a",
1.0,
duration,
)
_visibility_tween.finished.connect(
func() -> void:
if generation != _visibility_generation:
return
_visibility_tween = null
presentation_transition_finished.emit(true)
)
else:
if not visible:
return
if not animate:
visible = false
modulate.a = 1.0
presentation_transition_finished.emit(false)
return
var duration: float = (
transition_duration
if transition_duration >= 0.0
else UIMotion.UTILITY_EXIT_DURATION
)
_visibility_tween = create_tween()
_visibility_tween.tween_property(
self,
"modulate:a",
0.0,
duration,
)
_visibility_tween.finished.connect(
func() -> void:
if generation != _visibility_generation:
return
visible = false
modulate.a = 1.0
_visibility_tween = null
presentation_transition_finished.emit(false)
)
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
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
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:
_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
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.focus_entered.connect(
_on_controller_slot_focused.bind(slot.slot_index)
)
_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
)
_presentation_scale_root.position = _presentation_position()
2026-07-28 01:06:11 -04:00
var field_size := (
Vector2(580.0, 82.0)
2026-07-28 01:06:11 -04:00
if _compact_layout
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)
func _presentation_position() -> Vector2:
var scaled_size: Vector2 = (
DESKTOP_REFERENCE_SIZE * HOTBAR_PRESENTATION_SCALE
)
var bottom_centered_offset := Vector2(
(DESKTOP_REFERENCE_SIZE.x - scaled_size.x) * 0.5,
DESKTOP_REFERENCE_SIZE.y - scaled_size.y,
)
return bottom_centered_offset + (
HOTBAR_MENU_POSITION
if _player_menu_context
else HOTBAR_CANONICAL_POSITION
)
func _refresh() -> void:
2026-07-28 01:06:11 -04:00
for slot: BubbleHotbarSlotType in _slots:
slot.refresh()
func _on_selected_slot_changed(
_slot_index: int,
_item_id: StringName,
) -> void:
_refresh()
if _controller_placement_active:
_refresh_controller_placement_preview()
return
if _hovered_slot_index < 0:
_show_selected_item_briefly()
func _on_controller_slot_focused(slot_index: int) -> void:
2026-08-08 16:20:40 -04:00
if (
not (_controller_placement_active or _controller_management_active)
or _hotbar == null
):
return
_hotbar.select_slot(slot_index)
2026-08-08 16:20:40 -04:00
if _controller_placement_active:
_refresh_controller_placement_preview()
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)
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)
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:
_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
)
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()