straywild/ui/components/general_inventory_slot.gd

360 lines
10 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name GeneralInventorySlot
extends Button
signal entry_selected(kind: int, identity: StringName)
signal entry_moved
signal context_requested(slot: GeneralInventorySlot)
signal context_changed(slot: GeneralInventorySlot, text: String, active: bool)
const ItemDataType = preload("res://items/item_data.gd")
const LockedContentPresentationType = preload(
"res://ui/components/locked_content_presentation.gd"
)
var slot_index: int = -1
var container: int = -1
var entry_kind: int = -1
var entry_identity: StringName
var _locked: bool = false
var _layout: PlayerInventoryLayout
var _bag: PlayerBag
var _fish_inventory: FishInventory
var _hotbar: PlayerHotbar
var _item_catalog: ItemCatalog
var _icon: TextureRect
var _quantity: Label
var _presentation_size := Vector2(52.0, 52.0)
var _context_text: String = ""
var _context_hovered: bool = false
var _context_focused: bool = false
var _staged: bool = false
func _ready() -> void:
focus_mode = Control.FOCUS_ALL
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
pressed.connect(_on_pressed)
focus_entered.connect(_set_context_focused.bind(true))
focus_exited.connect(_set_context_focused.bind(false))
mouse_entered.connect(_set_context_hovered.bind(true))
mouse_exited.connect(_set_context_hovered.bind(false))
gui_input.connect(_on_gui_input)
_icon = TextureRect.new()
_icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_icon)
_quantity = Label.new()
_quantity.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_quantity.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_quantity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_quantity.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_quantity.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_quantity)
_apply_presentation()
func set_presentation_size(presentation_size: Vector2) -> void:
_presentation_size = presentation_size
custom_minimum_size = presentation_size
if is_node_ready():
_apply_presentation()
func configure(
new_slot_index: int,
new_container: int,
is_locked: bool,
layout: PlayerInventoryLayout,
bag: PlayerBag,
fish_inventory: FishInventory,
hotbar: PlayerHotbar,
item_catalog: ItemCatalog,
) -> void:
slot_index = new_slot_index
container = new_container
_locked = is_locked
_layout = layout
_bag = bag
_fish_inventory = fish_inventory
_hotbar = hotbar
_item_catalog = item_catalog
name = "InventorySlot%d" % slot_index
refresh()
func set_locked(is_locked: bool) -> void:
if _locked == is_locked:
return
_locked = is_locked
refresh()
func refresh() -> void:
call_deferred("_update_context_presence")
entry_kind = -1
entry_identity = StringName()
_context_text = ""
_icon.texture = null
_quantity.text = ""
disabled = _locked
_apply_icon_geometry()
if _locked:
_icon.texture = LockedContentPresentationType.ICON
_icon.modulate = LockedContentPresentationType.icon_modulate()
var storage_slot := (
container == PlayerInventoryLayout.InventoryContainer.STORAGE
)
_context_text = (
"locked storage slot" if storage_slot else "locked backpack slot"
)
tooltip_text = ""
accessibility_name = "%s %d" % [_context_text, slot_index + 1]
return
_icon.modulate = Color.WHITE
_context_text = "empty slot"
tooltip_text = ""
accessibility_name = "empty %s slot %d" % [
"storage"
if container == PlayerInventoryLayout.InventoryContainer.STORAGE
else "inventory",
slot_index + 1,
]
if _layout == null:
return
var entry := _layout.get_entry_at(container, slot_index)
if entry.is_empty():
return
entry_kind = int(entry.get("kind", -1))
entry_identity = StringName(str(entry.get("identity", "")))
if entry_kind == PlayerInventoryLayout.EntryKind.ITEM:
var item: ItemDataType = (
_item_catalog.get_item_by_id(entry_identity)
if _item_catalog != null else null
)
if item == null:
return
_icon.texture = item.icon
var quantity := _bag.get_quantity(entry_identity) if _bag != null else 0
_quantity.text = "×%d" % quantity if quantity > 1 else ""
_context_text = _item_context_text(item, quantity)
accessibility_name = "%s, slot %d" % [item.display_name, slot_index + 1]
elif entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
var fish_catch = (
_fish_inventory.get_catch_by_id(entry_identity)
if _fish_inventory != null else null
)
if fish_catch == null:
return
_icon.texture = fish_catch.fish.display_texture
_context_text = _catch_context_text(fish_catch)
var catch_name: String = FishQuality.qualified_name(
fish_catch.fish.display_name, fish_catch.quality
)
accessibility_name = "%s, slot %d" % [catch_name, slot_index + 1]
func _on_pressed() -> void:
if _locked:
return
entry_selected.emit(entry_kind, entry_identity)
func _on_gui_input(event: InputEvent) -> void:
var mouse_event := event as InputEventMouseButton
if mouse_event != null and mouse_event.pressed:
# Pointer clicks give Buttons keyboard focus after their GUI callback.
# Release that pointer-created focus on the next frame so a tooltip does
# not remain pinned after the pointer leaves. Controller navigation still
# keeps focus normally because it does not arrive as a mouse event.
call_deferred("_release_pointer_focus")
if (
mouse_event != null
and mouse_event.button_index == MOUSE_BUTTON_RIGHT
and mouse_event.pressed
and not _locked
and not entry_identity.is_empty()
):
context_requested.emit(self)
accept_event()
func _release_pointer_focus() -> void:
if has_focus():
release_focus()
func _set_context_hovered(active: bool) -> void:
_context_hovered = active
_update_context_presence()
func _set_context_focused(active: bool) -> void:
_context_focused = active
_update_context_presence()
func _update_context_presence() -> void:
if _locked or entry_identity.is_empty():
context_changed.emit(self, "", false)
return
var active: bool = _context_hovered or _context_focused
context_changed.emit(self, _context_text if active else "", active)
func set_move_source(active: bool) -> void:
modulate = Color(1.0, 1.0, 1.0, 0.42) if active else Color.WHITE
func set_staged(active: bool) -> void:
if _staged == active:
return
_staged = active
_apply_style()
func _item_context_text(item: ItemDataType, quantity: int) -> String:
var lines: Array[String] = [item.display_name]
lines.append("%s • quantity %d" % [item.get_category_name(), quantity])
if item.equippable:
lines.append("equippable")
elif item.usable:
lines.append("usable")
if not item.description.strip_edges().is_empty():
lines.append(item.description.strip_edges())
return "\n".join(lines)
func _catch_context_text(fish_catch: FishCatch) -> String:
var catch_name: String = FishQuality.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
)
return "%s\n%0.2f lb • value %d" % [
catch_name,
fish_catch.weight_lb,
fish_catch.sale_value,
]
func _get_drag_data(_at_position: Vector2) -> Variant:
if _locked or entry_identity.is_empty() or _icon.texture == null:
return null
var preview := TextureRect.new()
preview.custom_minimum_size = Vector2(56.0, 56.0)
preview.texture = _icon.texture
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)
if entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
return {
"kind": "cooler_fish",
"catch_id": String(entry_identity),
}
return {
"kind": "bag_item",
"item_id": String(entry_identity),
}
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if _locked or typeof(data) != TYPE_DICTIONARY or _layout == null:
return false
var kind := str((data as Dictionary).get("kind", ""))
return kind in ["bag_item", "cooler_fish", "hotbar_slot"]
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if not _can_drop_data(Vector2.ZERO, data):
return
var payload := data as Dictionary
var payload_kind := str(payload.get("kind", ""))
var moved: bool = false
if payload_kind == "hotbar_slot":
moved = (
_hotbar != null
and _hotbar.move_slot_to_container(
int(payload.get("slot_index", -1)),
container,
slot_index,
)
)
else:
var kind := (
PlayerInventoryLayout.EntryKind.CATCH
if payload_kind == "cooler_fish"
else PlayerInventoryLayout.EntryKind.ITEM
)
var identity := StringName(str(
payload.get(
"catch_id" if payload_kind == "cooler_fish" else "item_id",
"",
)
))
moved = _layout.move_entry(kind, identity, container, slot_index)
if moved:
entry_moved.emit()
func _apply_style() -> void:
var radius: int = roundi(minf(_presentation_size.x, _presentation_size.y) * 0.5)
var normal := UtilityPageStyle.rounded_style(
Color(
UtilityPageStyle.OCEAN_SELECTED
if _staged else UtilityPageStyle.OCEAN_FIELD,
0.92 if _staged else 0.88,
),
radius,
)
var hover := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_SELECTED, 0.92), radius
)
var locked := UtilityPageStyle.rounded_style(
LockedContentPresentationType.disabled_background_color(), radius
)
for state: StringName in [&"normal", &"pressed"]:
add_theme_stylebox_override(state, normal)
add_theme_stylebox_override("disabled", locked)
for state: StringName in [&"hover", &"focus"]:
add_theme_stylebox_override(state, hover)
func _apply_presentation() -> void:
_apply_icon_geometry()
var is_large: bool = _presentation_size.x >= 70.0
var quantity_width: float = 42.0 if is_large else 34.0
var quantity_height: float = 20.0 if is_large else 17.0
_quantity.position = Vector2(
-quantity_width - (8.0 if is_large else 5.0),
-quantity_height - (6.0 if is_large else 4.0),
)
_quantity.size = Vector2(quantity_width, quantity_height)
_quantity.add_theme_font_size_override("font_size", 14 if is_large else 11)
_apply_style()
func _apply_icon_geometry() -> void:
var is_large: bool = _presentation_size.x >= 70.0
var margin: float
if _locked:
var lock_size: float = LockedContentPresentationType.icon_size_for(
_presentation_size
).x
margin = maxf(
(_presentation_size.x - lock_size) * 0.5,
0.0,
)
else:
margin = 10.0 if is_large else 7.0
_icon.offset_left = margin
_icon.offset_top = margin
_icon.offset_right = -margin
_icon.offset_bottom = -margin