411 lines
11 KiB
GDScript
411 lines
11 KiB
GDScript
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 FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||
const FishQualityType = preload("res://fish/fish_quality.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(78.0, 78.0)
|
||
@export var compact_size: Vector2 = Vector2(54.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 _quantity_label: Label = %QuantityLabel
|
||
|
||
var _hotbar: PlayerHotbarType
|
||
var _bag: PlayerBagType
|
||
var _catalog: ItemCatalogType
|
||
var _fish_inventory: FishInventoryType
|
||
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)
|
||
_update_pivot()
|
||
_apply_style()
|
||
|
||
|
||
func setup(
|
||
hotbar: PlayerHotbarType,
|
||
bag: PlayerBagType,
|
||
catalog: ItemCatalogType,
|
||
fish_inventory: FishInventoryType,
|
||
) -> void:
|
||
_hotbar = hotbar
|
||
_bag = bag
|
||
_catalog = catalog
|
||
_fish_inventory = fish_inventory
|
||
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 catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||
var item: ItemDataType = (
|
||
_catalog.get_item_by_id(item_id)
|
||
if _catalog != null and not item_id.is_empty()
|
||
else null
|
||
)
|
||
var fish_catch: FishCatchType = (
|
||
_fish_inventory.get_catch_by_id(catch_id)
|
||
if _fish_inventory != null and not catch_id.is_empty()
|
||
else null
|
||
)
|
||
_item_icon.texture = (
|
||
fish_catch.fish.display_texture
|
||
if fish_catch != null
|
||
else 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 = (
|
||
"%s · %.1f lb" % [
|
||
FishQualityType.qualified_name(
|
||
fish_catch.fish.display_name,
|
||
fish_catch.quality,
|
||
),
|
||
fish_catch.weight_lb,
|
||
]
|
||
if fish_catch != null
|
||
else 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 and fish_catch == 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)
|
||
_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)
|
||
_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 selected_fill: Color = normal_fill
|
||
if _selected:
|
||
selected_fill = normal_fill.lightened(0.12)
|
||
add_theme_stylebox_override(
|
||
"normal",
|
||
_make_style(
|
||
selected_fill,
|
||
5 if _selected else 2,
|
||
0.38 if _selected else 0.20,
|
||
)
|
||
)
|
||
add_theme_stylebox_override(
|
||
"hover",
|
||
_make_style(
|
||
selected_fill.lightened(0.055),
|
||
5 if _selected else 3,
|
||
0.40 if _selected else 0.29,
|
||
)
|
||
)
|
||
add_theme_stylebox_override(
|
||
"pressed",
|
||
_make_style(
|
||
selected_fill.lightened(0.085),
|
||
6,
|
||
0.44,
|
||
)
|
||
)
|
||
add_theme_stylebox_override(
|
||
"focus",
|
||
get_theme_stylebox("normal")
|
||
)
|
||
add_theme_stylebox_override(
|
||
"disabled",
|
||
_make_style(
|
||
normal_fill,
|
||
1,
|
||
0.12,
|
||
)
|
||
)
|
||
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,
|
||
shadow_size: int,
|
||
shadow_alpha: float,
|
||
) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = fill_color
|
||
style.set_border_width_all(0)
|
||
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
|
||
style.shadow_color = Color(0.015, 0.06, 0.09, shadow_alpha)
|
||
style.shadow_size = shadow_size
|
||
style.shadow_offset = Vector2(0.0, 2.0)
|
||
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)
|
||
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||
if item_id.is_empty() and catch_id.is_empty():
|
||
return null
|
||
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
||
var fish_catch: FishCatchType = (
|
||
_fish_inventory.get_catch_by_id(catch_id)
|
||
if _fish_inventory != null and not catch_id.is_empty()
|
||
else null
|
||
)
|
||
var preview := TextureRect.new()
|
||
preview.custom_minimum_size = Vector2(44.0, 44.0)
|
||
preview.texture = (
|
||
fish_catch.fish.display_texture
|
||
if fish_catch != null
|
||
else 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,
|
||
}
|
||
|
||
|
||
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":
|
||
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
|
||
if kind == "cooler_fish":
|
||
var catch_id: StringName = StringName(str(payload.get("catch_id", "")))
|
||
return (
|
||
_fish_inventory != null
|
||
and _fish_inventory.contains_catch_id(catch_id)
|
||
)
|
||
return false
|
||
|
||
|
||
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)
|
||
elif str(payload.get("kind", "")) == "cooler_fish":
|
||
_hotbar.assign_fish(
|
||
slot_index,
|
||
StringName(str(payload["catch_id"])),
|
||
)
|
||
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:
|
||
var identity: StringName = _hotbar.get_item_id(slot_index)
|
||
if identity.is_empty():
|
||
identity = _hotbar.get_fish_catch_id(slot_index)
|
||
item_hovered.emit(slot_index, identity)
|
||
|
||
|
||
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()
|