模板脚本文件

This commit is contained in:
2026-09-12 22:18:23 +08:00
parent 2816092fb4
commit d3f1058d79
32 changed files with 3200 additions and 0 deletions
+422
View File
@@ -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)