straywild/ui/components/general_inventory_slot.gd

350 lines
10 KiB
GDScript3
Raw Normal View History

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.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 _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