模板脚本文件
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
class_name InventorySlotUI
|
||||
extends Control
|
||||
|
||||
signal slot_clicked(slot_index: int, button: int)
|
||||
signal slot_double_clicked(slot_index: int)
|
||||
signal item_hovered(slot_index: int, item: Item)
|
||||
signal item_unhovered
|
||||
|
||||
enum TYPE {
|
||||
INVENTORY,
|
||||
WEAPON,
|
||||
HAT,
|
||||
BACKPACK,
|
||||
}
|
||||
var slot_index: int = 0
|
||||
var slot_type: TYPE = TYPE.INVENTORY
|
||||
var inventory_data: InventorySlot
|
||||
var parent_inventory: Control
|
||||
|
||||
var _is_mouse_hovering := false
|
||||
|
||||
@onready var background: Panel = $Background
|
||||
@onready var item_icon: TextureRect = $ItemIcon
|
||||
@onready var quantity_label: Label = $QuantityLabel
|
||||
|
||||
|
||||
func _ready():
|
||||
gui_input.connect(_on_gui_input)
|
||||
mouse_entered.connect(_on_mouse_entered)
|
||||
mouse_exited.connect(_on_mouse_exited)
|
||||
|
||||
update_display()
|
||||
|
||||
|
||||
func set_slot_data(slot_data: InventorySlot, index: int):
|
||||
inventory_data = slot_data
|
||||
slot_index = index
|
||||
update_display()
|
||||
if is_node_ready() and _is_mouse_hovering:
|
||||
_emit_current_hover_state()
|
||||
|
||||
|
||||
func update_display():
|
||||
if not inventory_data or inventory_data.is_empty():
|
||||
_show_empty_slot()
|
||||
else:
|
||||
_show_item_slot()
|
||||
|
||||
|
||||
func _show_empty_slot():
|
||||
if item_icon:
|
||||
item_icon.texture = null
|
||||
if quantity_label:
|
||||
quantity_label.visible = false
|
||||
if background:
|
||||
background.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _show_item_slot():
|
||||
var item = ItemDatabase.get_item(inventory_data.item_id)
|
||||
if not item:
|
||||
_show_empty_slot()
|
||||
return
|
||||
|
||||
item_icon.texture = item.icon
|
||||
|
||||
if item.stackable and inventory_data.quantity > 1:
|
||||
quantity_label.text = str(inventory_data.quantity)
|
||||
quantity_label.visible = true
|
||||
else:
|
||||
quantity_label.visible = false
|
||||
|
||||
|
||||
func _on_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
|
||||
slot_double_clicked.emit(slot_index)
|
||||
accept_event()
|
||||
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
slot_clicked.emit(slot_index, event.button_index)
|
||||
accept_event()
|
||||
|
||||
|
||||
func _on_mouse_entered():
|
||||
_is_mouse_hovering = true
|
||||
_emit_current_hover_state()
|
||||
background.modulate = Color(1.2, 1.2, 1.2)
|
||||
|
||||
|
||||
func _on_mouse_exited():
|
||||
_is_mouse_hovering = false
|
||||
item_unhovered.emit()
|
||||
|
||||
background.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _emit_current_hover_state() -> void:
|
||||
if inventory_data and not inventory_data.is_empty():
|
||||
var item := ItemDatabase.get_item(inventory_data.item_id)
|
||||
if item:
|
||||
item_hovered.emit(slot_index, item)
|
||||
return
|
||||
item_unhovered.emit()
|
||||
|
||||
|
||||
func _can_drop_data(_position: Vector2, data) -> bool:
|
||||
return data is Dictionary and data.has("slot_index") and data.has("inventory_type")
|
||||
|
||||
|
||||
func _drop_data(_position: Vector2, data):
|
||||
if parent_inventory and parent_inventory.has_method("handle_item_drop") and slot_type == TYPE.INVENTORY:
|
||||
parent_inventory.handle_item_drop(data.slot_index, slot_index)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_weapon_equip") and slot_type == TYPE.WEAPON:
|
||||
parent_inventory.handle_weapon_equip(data.slot_index, data)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_hat_equip") and slot_type == TYPE.HAT:
|
||||
parent_inventory.handle_hat_equip(data.slot_index, data)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_backpack_equip") and slot_type == TYPE.BACKPACK:
|
||||
parent_inventory.handle_backpack_equip(data.slot_index, data)
|
||||
|
||||
|
||||
func _get_drag_data(_position: Vector2):
|
||||
if not inventory_data or inventory_data.is_empty():
|
||||
return null
|
||||
|
||||
var item = ItemDatabase.get_item(inventory_data.item_id)
|
||||
if not item:
|
||||
return null
|
||||
|
||||
var preview = Control.new()
|
||||
var preview_icon = TextureRect.new()
|
||||
preview_icon.texture = item.icon
|
||||
preview_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
preview_icon.size = Vector2(32, 32)
|
||||
preview.add_child(preview_icon)
|
||||
|
||||
preview.modulate = Color(1, 1, 1, 0.8)
|
||||
set_drag_preview(preview)
|
||||
|
||||
item_icon.modulate = Color(0.5, 0.5, 0.5)
|
||||
|
||||
return {"slot_index": slot_index, "inventory_type": item.item_type}
|
||||
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_DRAG_END:
|
||||
item_icon.modulate = Color.WHITE
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi0mlil0a0e5i
|
||||
@@ -0,0 +1,422 @@
|
||||
class_name InventoryUI
|
||||
extends Control
|
||||
|
||||
signal inventory_closed
|
||||
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
const TOOLTIP_MAX_WIDTH := 280.0
|
||||
const TOOLTIP_MAX_HEIGHT := 240.0
|
||||
const TOOLTIP_SCREEN_MARGIN := 10.0
|
||||
const TOOLTIP_HORIZONTAL_PADDING := 16.0
|
||||
const TOOLTIP_VERTICAL_PADDING := 16.0
|
||||
|
||||
const SLOT_INDEX_WEAPON := -1
|
||||
const SLOT_INDEX_HAT := -2
|
||||
const SLOT_INDEX_BACKPACK := -3
|
||||
|
||||
var current_player: Character
|
||||
var slot_ui_scene: PackedScene
|
||||
var slot_uis: Array[InventorySlotUI] = []
|
||||
var current_item: Item
|
||||
var current_slot_index: int
|
||||
var weapon_slot_ui: InventorySlotUI
|
||||
var hat_slot_ui: InventorySlotUI
|
||||
var backpack_slot_ui: InventorySlotUI
|
||||
var _tooltip_layout_request := 0
|
||||
var _tooltip_should_be_visible := false
|
||||
|
||||
@onready var grid_container: GridContainer = get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/GridContainer"
|
||||
)
|
||||
@onready var close_button: Button = get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar/CloseButton"
|
||||
)
|
||||
@onready var tooltip: Control = $ItemTooltip
|
||||
@onready var tooltip_label: RichTextLabel = $ItemTooltip/Panel/MarginContainer/TooltipText
|
||||
@onready var inventory_panels: HBoxContainer = $SafeArea/CenterContainer/InventoryPanels
|
||||
|
||||
|
||||
func _ready():
|
||||
slot_ui_scene = preload("res://scenes/ui/inventory_slot_ui.tscn")
|
||||
grid_container.columns = 4
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
tooltip.visible = false
|
||||
_create_slot_uis()
|
||||
_create_hat_slot_ui()
|
||||
_create_weapon_slot_ui()
|
||||
_create_backpack_slot_ui()
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _create_hat_slot_ui():
|
||||
hat_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
hat_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
hat_slot_ui.parent_inventory = self
|
||||
hat_slot_ui.slot_type = InventorySlotUI.TYPE.HAT
|
||||
hat_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
hat_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
hat_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/HatSlotContainer"
|
||||
)
|
||||
. add_child(hat_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_weapon_slot_ui():
|
||||
weapon_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
weapon_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
weapon_slot_ui.parent_inventory = self
|
||||
weapon_slot_ui.slot_type = InventorySlotUI.TYPE.WEAPON
|
||||
weapon_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
weapon_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
weapon_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/WeaponSlotContainer"
|
||||
)
|
||||
. add_child(weapon_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_backpack_slot_ui() -> void:
|
||||
backpack_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
backpack_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
backpack_slot_ui.parent_inventory = self
|
||||
backpack_slot_ui.slot_type = InventorySlotUI.TYPE.BACKPACK
|
||||
backpack_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
backpack_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
backpack_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/BackpackSlotContainer"
|
||||
)
|
||||
. add_child(backpack_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_slot_uis():
|
||||
for child in grid_container.get_children():
|
||||
child.queue_free()
|
||||
slot_uis.clear()
|
||||
|
||||
for i in range(PlayerInventory.MAX_INVENTORY_SIZE):
|
||||
var slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
slot_ui.parent_inventory = self
|
||||
slot_ui.visible = i < PlayerInventory.BASE_INVENTORY_SIZE
|
||||
|
||||
slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
slot_ui.slot_double_clicked.connect(_on_slot_double_clicked)
|
||||
slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
|
||||
slot_ui.set_slot_data(null, i)
|
||||
|
||||
grid_container.add_child(slot_ui)
|
||||
slot_uis.append(slot_ui)
|
||||
|
||||
|
||||
func update_inventory_display():
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
var player_inventory = current_player.get_inventory()
|
||||
var active_slot_count := player_inventory.get_active_slot_count()
|
||||
for i in range(slot_uis.size()):
|
||||
slot_uis[i].visible = i < active_slot_count
|
||||
if i < active_slot_count:
|
||||
slot_uis[i].set_slot_data(player_inventory.get_slot(i), i)
|
||||
else:
|
||||
slot_uis[i].set_slot_data(null, i)
|
||||
weapon_slot_ui.set_slot_data(player_inventory.equipped_weapon, SLOT_INDEX_WEAPON)
|
||||
hat_slot_ui.set_slot_data(player_inventory.equipped_hat, SLOT_INDEX_HAT)
|
||||
backpack_slot_ui.set_slot_data(player_inventory.equipped_backpack, SLOT_INDEX_BACKPACK)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _on_slot_clicked(slot_index: int, button: int):
|
||||
if button == MOUSE_BUTTON_RIGHT:
|
||||
_handle_right_click(slot_index)
|
||||
|
||||
|
||||
func _on_slot_double_clicked(slot_index: int) -> void:
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
if slot_index < 0 or not current_player.get_inventory().is_slot_active(slot_index):
|
||||
return
|
||||
|
||||
var slot := current_player.get_inventory().get_slot(slot_index)
|
||||
if not slot or slot.is_empty():
|
||||
return
|
||||
|
||||
var item := ItemDatabase.get_item(slot.item_id)
|
||||
if not item or not item.context_options.has(Item.ContextOptions.EQUIP):
|
||||
return
|
||||
if (
|
||||
item.item_type != Item.ItemType.WEAPON
|
||||
and item.item_type != Item.ItemType.HAT
|
||||
and item.item_type != Item.ItemType.BACKPACK
|
||||
):
|
||||
return
|
||||
|
||||
current_player.request_equip_item.rpc_id(1, slot_index, item.item_type)
|
||||
|
||||
|
||||
func _handle_right_click(slot_index: int):
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
var player_inventory = current_player.get_inventory()
|
||||
current_slot_index = slot_index
|
||||
var slot: InventorySlot
|
||||
if slot_index == SLOT_INDEX_WEAPON:
|
||||
slot = player_inventory.equipped_weapon
|
||||
elif slot_index == SLOT_INDEX_HAT:
|
||||
slot = player_inventory.equipped_hat
|
||||
elif slot_index == SLOT_INDEX_BACKPACK:
|
||||
slot = player_inventory.equipped_backpack
|
||||
else:
|
||||
slot = player_inventory.get_slot(slot_index)
|
||||
if slot and not slot.is_empty():
|
||||
current_item = ItemDatabase.get_item(slot.item_id)
|
||||
if current_item:
|
||||
_hide_tooltip()
|
||||
var context_menu = PopupMenu.new()
|
||||
add_child(context_menu)
|
||||
context_menu.popup_hide.connect(context_menu.queue_free)
|
||||
context_menu.id_pressed.connect(_on_item_selected)
|
||||
if slot_index == SLOT_INDEX_WEAPON or slot_index == SLOT_INDEX_HAT or slot_index == SLOT_INDEX_BACKPACK:
|
||||
context_menu.add_item(
|
||||
_get_context_menu_string(Item.ContextOptions.UNEQUIP), Item.ContextOptions.UNEQUIP
|
||||
)
|
||||
else:
|
||||
for item_option in current_item.context_options:
|
||||
context_menu.add_item(_get_context_menu_string(item_option), item_option)
|
||||
|
||||
context_menu.set_position(get_viewport().get_mouse_position())
|
||||
context_menu.popup()
|
||||
|
||||
|
||||
func _on_item_selected(index: int):
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
if index == Item.ContextOptions.EQUIP:
|
||||
current_player.request_equip_item.rpc_id(1, current_slot_index, current_item.item_type)
|
||||
elif index == Item.ContextOptions.UNEQUIP:
|
||||
current_player.request_unequip_item.rpc_id(1, current_item.item_type)
|
||||
elif index == Item.ContextOptions.DROP:
|
||||
if current_item.scene_path.is_empty() or not ResourceLoader.exists(current_item.scene_path):
|
||||
push_warning(
|
||||
"Cannot drop item '" + current_item.id + "': invalid scene path '" + current_item.scene_path + "'"
|
||||
)
|
||||
return
|
||||
current_player.add_world_item.rpc_id(
|
||||
1, current_item.scene_path, current_player.get_node("GodotRobot3D/InfrontArea3D").global_position
|
||||
)
|
||||
current_player.request_remove_item.rpc_id(1, current_item.id, 1)
|
||||
refresh_display()
|
||||
|
||||
|
||||
func _on_item_hovered(_slot_index: int, item: Item):
|
||||
_show_tooltip(item)
|
||||
|
||||
|
||||
func _on_item_unhovered():
|
||||
_hide_tooltip()
|
||||
|
||||
|
||||
func _show_tooltip(item: Item):
|
||||
if not item:
|
||||
return
|
||||
|
||||
var tooltip_content = "[font_size=18][b][color=#FFD700]" + item.name + "[/color][/b][/font_size]\n"
|
||||
tooltip_content += "[color=#CCCCCC]" + item.description + "[/color]\n"
|
||||
tooltip_content += "[color=#87CEEB]Type:[/color] " + _get_item_type_string(item.item_type) + "\n"
|
||||
tooltip_content += "[color=#FF69B4]Rarity:[/color] " + _get_rarity_string(item.rarity) + "\n"
|
||||
tooltip_content += "[color=#FFD700]Value:[/color] " + str(item.value) + " gold"
|
||||
|
||||
if item.stackable:
|
||||
tooltip_content += "\n[color=#98FB98]Max Stack:[/color] " + str(item.max_stack)
|
||||
|
||||
tooltip_label.text = tooltip_content
|
||||
_tooltip_should_be_visible = true
|
||||
_queue_tooltip_layout()
|
||||
|
||||
|
||||
func _hide_tooltip():
|
||||
_tooltip_should_be_visible = false
|
||||
_tooltip_layout_request += 1
|
||||
tooltip.visible = false
|
||||
tooltip.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _queue_tooltip_layout() -> void:
|
||||
_set_tooltip_width()
|
||||
_tooltip_layout_request += 1
|
||||
var request_id := _tooltip_layout_request
|
||||
tooltip.modulate = Color(1.0, 1.0, 1.0, 0.0)
|
||||
tooltip.visible = true
|
||||
_apply_tooltip_layout_after_frame(request_id)
|
||||
|
||||
|
||||
func _apply_tooltip_layout_after_frame(request_id: int) -> void:
|
||||
await get_tree().process_frame
|
||||
if request_id != _tooltip_layout_request or not _tooltip_should_be_visible or not tooltip or not tooltip_label:
|
||||
return
|
||||
|
||||
var tooltip_size := tooltip.size
|
||||
var available_height := minf(TOOLTIP_MAX_HEIGHT, maxf(1.0, size.y - TOOLTIP_SCREEN_MARGIN * 2.0))
|
||||
tooltip_size.y = minf(float(tooltip_label.get_content_height()) + TOOLTIP_VERTICAL_PADDING, available_height)
|
||||
tooltip.size = tooltip_size
|
||||
_position_tooltip_smartly()
|
||||
tooltip.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _set_tooltip_width() -> void:
|
||||
var tooltip_size := tooltip.size
|
||||
var available_width := maxf(1.0, size.x - TOOLTIP_SCREEN_MARGIN * 2.0)
|
||||
tooltip_size.x = minf(TOOLTIP_MAX_WIDTH, available_width)
|
||||
tooltip.size = tooltip_size
|
||||
var label_minimum_size := tooltip_label.custom_minimum_size
|
||||
label_minimum_size.x = maxf(1.0, tooltip_size.x - TOOLTIP_HORIZONTAL_PADDING)
|
||||
tooltip_label.custom_minimum_size = label_minimum_size
|
||||
var label_size := tooltip_label.size
|
||||
label_size.x = label_minimum_size.x
|
||||
tooltip_label.size = label_size
|
||||
|
||||
|
||||
func _position_tooltip_smartly():
|
||||
var mouse_pos = get_global_mouse_position()
|
||||
var tooltip_size = tooltip.size
|
||||
|
||||
var viewport_size = get_viewport().get_visible_rect().size
|
||||
var tooltip_pos = mouse_pos + Vector2.ONE * TOOLTIP_SCREEN_MARGIN
|
||||
if tooltip_pos.x + tooltip_size.x > viewport_size.x - TOOLTIP_SCREEN_MARGIN:
|
||||
tooltip_pos.x = mouse_pos.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN
|
||||
if tooltip_pos.y + tooltip_size.y > viewport_size.y - TOOLTIP_SCREEN_MARGIN:
|
||||
tooltip_pos.y = mouse_pos.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN
|
||||
|
||||
var max_x := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN)
|
||||
var max_y := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN)
|
||||
tooltip_pos.x = clampf(tooltip_pos.x, TOOLTIP_SCREEN_MARGIN, max_x)
|
||||
tooltip_pos.y = clampf(tooltip_pos.y, TOOLTIP_SCREEN_MARGIN, max_y)
|
||||
|
||||
tooltip.global_position = tooltip_pos
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not inventory_panels or not tooltip:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var content_size := inventory_panels.size
|
||||
if content_size.x > 0.0 and content_size.y > 0.0:
|
||||
var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
|
||||
inventory_panels.pivot_offset = content_size * 0.5
|
||||
inventory_panels.scale = Vector2.ONE * scale_factor
|
||||
|
||||
_set_tooltip_width()
|
||||
if _tooltip_should_be_visible:
|
||||
_queue_tooltip_layout()
|
||||
|
||||
|
||||
func _get_item_type_string(type: Item.ItemType) -> String:
|
||||
var type_name := "Unknown"
|
||||
match type:
|
||||
Item.ItemType.WEAPON:
|
||||
type_name = "Weapon"
|
||||
Item.ItemType.HAT:
|
||||
type_name = "Hat"
|
||||
Item.ItemType.BACKPACK:
|
||||
type_name = "Backpack"
|
||||
Item.ItemType.CONSUMABLE:
|
||||
type_name = "Consumable"
|
||||
Item.ItemType.TOOL:
|
||||
type_name = "Tool"
|
||||
Item.ItemType.MISC:
|
||||
type_name = "Miscellaneous"
|
||||
return type_name
|
||||
|
||||
|
||||
func _get_rarity_string(rarity: Item.ItemRarity) -> String:
|
||||
match rarity:
|
||||
Item.ItemRarity.COMMON:
|
||||
return "Common"
|
||||
Item.ItemRarity.UNCOMMON:
|
||||
return "Uncommon"
|
||||
Item.ItemRarity.RARE:
|
||||
return "Rare"
|
||||
Item.ItemRarity.EPIC:
|
||||
return "Epic"
|
||||
Item.ItemRarity.LEGENDARY:
|
||||
return "Legendary"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func _get_context_menu_string(context: Item.ContextOptions) -> String:
|
||||
match context:
|
||||
Item.ContextOptions.DROP:
|
||||
return "Drop"
|
||||
Item.ContextOptions.EQUIP:
|
||||
return "Equip"
|
||||
Item.ContextOptions.UNEQUIP:
|
||||
return "Unequip"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func handle_item_drop(from_slot: int, to_slot: int):
|
||||
if not current_player:
|
||||
return
|
||||
if from_slot == SLOT_INDEX_WEAPON:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.WEAPON, to_slot)
|
||||
elif from_slot == SLOT_INDEX_HAT:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.HAT, to_slot)
|
||||
elif from_slot == SLOT_INDEX_BACKPACK:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.BACKPACK, to_slot)
|
||||
else:
|
||||
current_player.request_move_item.rpc_id(1, from_slot, to_slot)
|
||||
|
||||
|
||||
func _on_close_pressed():
|
||||
_hide_tooltip()
|
||||
inventory_closed.emit()
|
||||
visible = false
|
||||
|
||||
|
||||
func open_inventory(player: Character = null):
|
||||
_hide_tooltip()
|
||||
if player:
|
||||
current_player = player
|
||||
update_inventory_display()
|
||||
visible = true
|
||||
|
||||
|
||||
func close_inventory():
|
||||
_hide_tooltip()
|
||||
visible = false
|
||||
|
||||
|
||||
func refresh_display():
|
||||
update_inventory_display()
|
||||
|
||||
|
||||
func handle_weapon_equip(from_slot: int, item: Dictionary):
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.WEAPON:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.WEAPON)
|
||||
|
||||
|
||||
func handle_hat_equip(from_slot: int, item: Dictionary):
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.HAT:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.HAT)
|
||||
|
||||
|
||||
func handle_backpack_equip(from_slot: int, item: Dictionary) -> void:
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.BACKPACK:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.BACKPACK)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2w12kgcvtww4
|
||||
@@ -0,0 +1,71 @@
|
||||
class_name MainMenuUI
|
||||
extends Control
|
||||
|
||||
signal host_pressed(nickname: String, skin: String)
|
||||
signal join_pressed(nickname: String, skin: String, address: String)
|
||||
signal quit_pressed
|
||||
|
||||
const SKIN_OPTIONS: Array[String] = ["Blue", "Yellow", "Green", "Red"]
|
||||
const SAFE_AREA_MARGIN := 24.0
|
||||
|
||||
@onready var skin_input: OptionButton = $MainContainer/MainMenu/Option2/SkinInput
|
||||
@onready var nick_input: LineEdit = $MainContainer/MainMenu/Option1/NickInput
|
||||
@onready var address_input: LineEdit = $MainContainer/MainMenu/Option3/AddressInput
|
||||
@onready var main_container: VBoxContainer = $MainContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
skin_input.clear()
|
||||
for skin_name in SKIN_OPTIONS:
|
||||
skin_input.add_item(skin_name)
|
||||
skin_input.select(0)
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _on_host_pressed() -> void:
|
||||
var nickname = nick_input.text.strip_edges()
|
||||
var skin = get_skin()
|
||||
host_pressed.emit(nickname, skin)
|
||||
|
||||
|
||||
func _on_join_pressed() -> void:
|
||||
var nickname = nick_input.text.strip_edges()
|
||||
var skin = get_skin()
|
||||
var address = address_input.text.strip_edges()
|
||||
join_pressed.emit(nickname, skin, address)
|
||||
|
||||
|
||||
func _on_quit_pressed():
|
||||
quit_pressed.emit()
|
||||
|
||||
|
||||
func show_menu():
|
||||
show()
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func hide_menu():
|
||||
hide()
|
||||
|
||||
|
||||
func is_menu_visible() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not main_container:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var content_size := main_container.get_combined_minimum_size()
|
||||
if content_size.x <= 0.0 or content_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
|
||||
main_container.pivot_offset = main_container.size * 0.5
|
||||
main_container.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func get_skin() -> String:
|
||||
return skin_input.get_item_text(skin_input.selected).to_lower()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c9x8y7z6a5b4c3d2
|
||||
@@ -0,0 +1,155 @@
|
||||
class_name MultiplayerChatUI
|
||||
extends Control
|
||||
|
||||
signal message_sent(message_text: String)
|
||||
|
||||
const MAX_CHAT_MESSAGES: int = 100
|
||||
const MAX_VISIBLE_MESSAGES: int = 8
|
||||
const FEED_VISIBLE_SECONDS: float = 6.0
|
||||
const FEED_FADE_SECONDS: float = 0.35
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
const MIN_CHAT_WIDTH := 280.0
|
||||
const MAX_CHAT_WIDTH := 480.0
|
||||
const MAX_CHAT_HEIGHT := 268.0
|
||||
|
||||
var chat_visible: bool = false
|
||||
var chat_history: Array[String] = []
|
||||
var fade_tween: Tween
|
||||
|
||||
@onready var chat: RichTextLabel = $ChatLayout/Chat
|
||||
@onready var message: LineEdit = $ChatLayout/Message
|
||||
@onready var feed_timer: Timer = $FeedTimer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message.text_submitted.connect(_on_message_submitted)
|
||||
feed_timer.timeout.connect(_on_feed_timer_timeout)
|
||||
feed_timer.wait_time = FEED_VISIBLE_SECONDS
|
||||
get_viewport().size_changed.connect(_update_responsive_layout)
|
||||
_update_responsive_layout()
|
||||
show()
|
||||
clear_chat()
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
var viewport_size := get_viewport().get_visible_rect().size
|
||||
var available_width := maxf(1.0, viewport_size.x - SAFE_AREA_MARGIN * 2.0)
|
||||
var preferred_width := clampf(viewport_size.x * 0.42, MIN_CHAT_WIDTH, MAX_CHAT_WIDTH)
|
||||
var target_width := minf(available_width, preferred_width)
|
||||
var target_height := minf(MAX_CHAT_HEIGHT, maxf(1.0, viewport_size.y - SAFE_AREA_MARGIN * 2.0))
|
||||
|
||||
offset_left = SAFE_AREA_MARGIN
|
||||
offset_right = SAFE_AREA_MARGIN + target_width
|
||||
offset_bottom = -SAFE_AREA_MARGIN
|
||||
offset_top = -SAFE_AREA_MARGIN - target_height
|
||||
|
||||
|
||||
func toggle_chat() -> void:
|
||||
if chat_visible:
|
||||
close_chat()
|
||||
return
|
||||
|
||||
chat_visible = true
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
message.show()
|
||||
await get_tree().process_frame
|
||||
if not chat_visible:
|
||||
return
|
||||
message.grab_focus()
|
||||
|
||||
|
||||
func close_chat() -> void:
|
||||
chat_visible = false
|
||||
message.text = ""
|
||||
message.release_focus()
|
||||
message.hide()
|
||||
|
||||
if chat_history.is_empty():
|
||||
chat.hide()
|
||||
else:
|
||||
_show_feed_temporarily()
|
||||
|
||||
|
||||
func is_chat_visible() -> bool:
|
||||
return chat_visible
|
||||
|
||||
|
||||
func _on_message_submitted(_submitted_text: String) -> void:
|
||||
var message_text := message.text.strip_edges()
|
||||
if message_text.is_empty():
|
||||
return
|
||||
|
||||
close_chat()
|
||||
message_sent.emit(message_text)
|
||||
|
||||
|
||||
func add_message(nick: String, msg: String) -> void:
|
||||
var formatted_message := _escape_bbcode("%s: %s" % [nick, msg])
|
||||
chat_history.append(formatted_message)
|
||||
while chat_history.size() > MAX_CHAT_MESSAGES:
|
||||
chat_history.pop_front()
|
||||
|
||||
_render_chat_history()
|
||||
if chat_visible:
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
else:
|
||||
_show_feed_temporarily()
|
||||
|
||||
|
||||
func _render_chat_history() -> void:
|
||||
chat.clear()
|
||||
var first_message := maxi(0, chat_history.size() - MAX_VISIBLE_MESSAGES)
|
||||
for index in range(first_message, chat_history.size()):
|
||||
chat.append_text(chat_history[index])
|
||||
if index < chat_history.size() - 1:
|
||||
chat.append_text("\n")
|
||||
|
||||
|
||||
func _show_feed_temporarily() -> void:
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
feed_timer.start()
|
||||
|
||||
|
||||
func _show_feed() -> void:
|
||||
chat.modulate.a = 1.0
|
||||
chat.show()
|
||||
|
||||
|
||||
func _on_feed_timer_timeout() -> void:
|
||||
if chat_visible:
|
||||
return
|
||||
|
||||
_stop_fade_tween()
|
||||
fade_tween = create_tween()
|
||||
fade_tween.tween_property(chat, "modulate:a", 0.0, FEED_FADE_SECONDS)
|
||||
fade_tween.tween_callback(chat.hide)
|
||||
|
||||
|
||||
func _stop_feed_animation() -> void:
|
||||
feed_timer.stop()
|
||||
_stop_fade_tween()
|
||||
|
||||
|
||||
func _stop_fade_tween() -> void:
|
||||
if fade_tween and fade_tween.is_valid():
|
||||
fade_tween.kill()
|
||||
fade_tween = null
|
||||
|
||||
|
||||
func _escape_bbcode(text: String) -> String:
|
||||
return text.replace("[", "[lb]")
|
||||
|
||||
|
||||
func clear_chat() -> void:
|
||||
_stop_feed_animation()
|
||||
chat_history.clear()
|
||||
chat.clear()
|
||||
chat.modulate.a = 1.0
|
||||
chat.hide()
|
||||
message.text = ""
|
||||
message.release_focus()
|
||||
message.hide()
|
||||
chat_visible = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8x9y2z1a3b4c5d6
|
||||
@@ -0,0 +1,57 @@
|
||||
class_name PauseMenuUI
|
||||
extends Control
|
||||
|
||||
signal resume_pressed
|
||||
signal main_menu_pressed
|
||||
signal quit_pressed
|
||||
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
|
||||
@onready var resume_button: Button = $Overlay/Panel/MarginContainer/Buttons/Resume
|
||||
@onready var panel: PanelContainer = $Overlay/Panel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
hide()
|
||||
|
||||
|
||||
func show_menu() -> void:
|
||||
show()
|
||||
call_deferred("_update_responsive_layout")
|
||||
resume_button.grab_focus()
|
||||
|
||||
|
||||
func hide_menu() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func is_menu_visible() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not panel:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var panel_size := panel.size
|
||||
if panel_size.x <= 0.0 or panel_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
|
||||
panel.pivot_offset = panel_size * 0.5
|
||||
panel.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func _on_resume_pressed() -> void:
|
||||
resume_pressed.emit()
|
||||
|
||||
|
||||
func _on_main_menu_pressed() -> void:
|
||||
main_menu_pressed.emit()
|
||||
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
quit_pressed.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://wbiqr2q6c47q
|
||||
@@ -0,0 +1,109 @@
|
||||
class_name PlayerListUI
|
||||
extends Control
|
||||
|
||||
const PLAYER_ROW_HEIGHT := 28.0
|
||||
const MAX_VISIBLE_ROWS := 8
|
||||
const RESERVED_VERTICAL_SPACE := 190.0
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
|
||||
var _player_count := 0
|
||||
|
||||
@onready var panel: PanelContainer = $SafeArea/PositionContainer/Panel
|
||||
@onready var count_label: Label = $SafeArea/PositionContainer/Panel/MarginContainer/Content/Count
|
||||
@onready var player_scroll: ScrollContainer = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll
|
||||
@onready
|
||||
var players_label: RichTextLabel = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll/Players
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resized.connect(_update_layout)
|
||||
_update_layout()
|
||||
hide()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible or not (event is InputEventMouseButton):
|
||||
return
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if not mouse_event.pressed:
|
||||
return
|
||||
if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
player_scroll.scroll_vertical -= int(PLAYER_ROW_HEIGHT)
|
||||
elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
player_scroll.scroll_vertical += int(PLAYER_ROW_HEIGHT)
|
||||
|
||||
|
||||
func show_players(players: Dictionary, local_peer_id: int) -> void:
|
||||
refresh_players(players, local_peer_id)
|
||||
player_scroll.scroll_vertical = 0
|
||||
show()
|
||||
call_deferred("_apply_panel_scale")
|
||||
|
||||
|
||||
func hide_players() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func refresh_players(players: Dictionary, local_peer_id: int) -> void:
|
||||
var peer_ids: Array[int] = []
|
||||
for peer_id_value in players:
|
||||
var peer_id := int(peer_id_value)
|
||||
if peer_id > 0:
|
||||
peer_ids.append(peer_id)
|
||||
peer_ids.sort()
|
||||
_player_count = peer_ids.size()
|
||||
|
||||
count_label.text = "%d online" % _player_count
|
||||
players_label.clear()
|
||||
for index in range(peer_ids.size()):
|
||||
var peer_id := peer_ids[index]
|
||||
var player_info: Dictionary = players.get(peer_id, {})
|
||||
var nickname := Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(peer_id))
|
||||
var local_marker := " [color=#c7ccd4](you)[/color]" if peer_id == local_peer_id else ""
|
||||
players_label.append_text(
|
||||
(
|
||||
"[color=#a8adb5]>[/color] [b]%s[/b]%s [color=#9aa0a8]#%d[/color]"
|
||||
% [_escape_bbcode(nickname), local_marker, peer_id]
|
||||
)
|
||||
)
|
||||
if index < peer_ids.size() - 1:
|
||||
players_label.append_text("\n")
|
||||
_update_layout()
|
||||
|
||||
|
||||
func _update_layout() -> void:
|
||||
if not panel or not player_scroll:
|
||||
return
|
||||
var panel_minimum_size := panel.custom_minimum_size
|
||||
panel_minimum_size.x = maxf(0.0, minf(420.0, size.x - 32.0))
|
||||
panel.custom_minimum_size = panel_minimum_size
|
||||
|
||||
var visible_rows := maxi(1, mini(_player_count, MAX_VISIBLE_ROWS))
|
||||
var desired_height := float(visible_rows) * PLAYER_ROW_HEIGHT
|
||||
var viewport_limit := maxf(30.0, size.y - RESERVED_VERTICAL_SPACE)
|
||||
var scroll_minimum_size := player_scroll.custom_minimum_size
|
||||
scroll_minimum_size.y = minf(desired_height, viewport_limit)
|
||||
player_scroll.custom_minimum_size = scroll_minimum_size
|
||||
|
||||
var players_minimum_size := players_label.custom_minimum_size
|
||||
players_minimum_size.y = float(maxi(1, _player_count)) * PLAYER_ROW_HEIGHT
|
||||
players_label.custom_minimum_size = players_minimum_size
|
||||
call_deferred("_apply_panel_scale")
|
||||
|
||||
|
||||
func _apply_panel_scale() -> void:
|
||||
if not panel:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var panel_size := panel.size
|
||||
if panel_size.x <= 0.0 or panel_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
|
||||
panel.pivot_offset = panel_size * 0.5
|
||||
panel.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func _escape_bbcode(text: String) -> String:
|
||||
return text.replace("[", "[lb]")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpxhd38sedg8u
|
||||
Reference in New Issue
Block a user