# @tool class_name PropHud extends Control signal current_item_changed(prop_key: String) @export_group("DebugItem") @export var enable_item := false: set(value): if value: enable_item = false enable_prop_item(item_key) @export var item_key: String @export_group("Inventory") @export var inventory: PropInventory: set(value): inventory = value if inventory and not inventory.current_item_changed.is_connected(current_item_changed.emit): inventory.current_item_changed.connect(current_item_changed.emit) @export_group("UI-UX") @export var display_time := 2.5 # 不包含渐入渐出(约 0.6s)的时长 @export var locked := false: set(value): locked = value if value: selected = false @export var selected := false: set(value): selected = value @onready var sfx_click = %SfxClick as Sfx @onready var left_btn = %LeftButton as TextureButton @onready var right_btn = %RightButton as TextureButton @onready var title_label = %TitleLabel as Label @onready var hud_rect = %HudRect as NinePatchRect @onready var props_scroll = %PropScrollContainer as ScrollContainer @onready var props_hbox = %PropsHBox as HBoxContainer var prop_containers = [] #CenterContainer const PROP_CONTAINER_X = 130.0 const PROP_CONTROL_X = 110.0 var items_dict := {} # 从配置文件加载 prop items var item_config_res = preload("res://asset/dialogue/item_description.dialogue") var path_prefix = "res://asset/art/prop/" var cached_inventory_textures := {} var listen_mouse = false var displaying = false var timer := Timer.new() var display_tween: Tween var container_tween: Tween func _ready() -> void: if Engine.is_editor_hint(): return # read prop containers,第一个为 head 跳过, 最后一个为 tail 跳过 for id in range(props_hbox.get_child_count() - 2): var container = props_hbox.get_child(id + 1) prop_containers.append(container) container.get_child(0).get_child(0).pressed.connect(_on_prop_pressed.bind(id)) _load_items() _load_from_archive() props_scroll.scroll_horizontal = PROP_CONTAINER_X focus_exited.connect(_on_focus_exited) # 存档更新时,从存档加载 prop ArchiveManager.archive_loaded.connect(_load_from_archive) # tween timer timer.wait_time = display_time timer.one_shot = true timer.autostart = false timer.timeout.connect(_on_timer_timeout) add_child(timer) # connect signals left_btn.pressed.connect(on_left_pressed) right_btn.pressed.connect(on_right_pressed) title_label.modulate.a = 0.0 # _toggle_btn_ability(false) left_btn.modulate.a = 0.5 right_btn.modulate.a = 0.5 mouse_entered.connect(_on_mouse_entered) mouse_exited.connect(_on_mouse_exited) func _on_focus_exited() -> void: if selected: selected = false func _load_items(): var id = item_config_res.titles["PropItems"] var current_line = item_config_res.lines[id] while current_line: if current_line.has("tags") and current_line.has("translation_key"): var wrapped_texture := "texture=" var wrapped_inspect := "inspect=" var texture_path = "" var inspect_path = "" for t in current_line.tags: if t.begins_with(wrapped_texture): texture_path = t.replace(wrapped_texture, "").strip_edges() elif t.begins_with(wrapped_inspect): inspect_path = t.replace(wrapped_inspect, "").strip_edges() var item = PropItem.new() item.key = current_line.translation_key if texture_path: item.texture_path = path_prefix + texture_path if inspect_path: item.inspect_path = path_prefix + inspect_path items_dict[item.key] = item if not current_line.has("next_id") or current_line.next_id == "end": break current_line = item_config_res.lines[current_line.next_id] func _load_from_archive() -> void: if ArchiveManager.archive: inventory = ArchiveManager.archive.prop_inventory _align_container_size() _load_texture_cache() _update_prop_display_with_texture() func _load_texture_cache() -> void: if not inventory: return for k in cached_inventory_textures: # remove invalid textures if not k in inventory.enabled_items: cached_inventory_textures.erase(k) for key in inventory.enabled_items: if cached_inventory_textures.has(key): continue # 以 items_dict 为准,如果 enabled_items 中的 key 不存在,则删除 if not key in items_dict: inventory.enabled_items.erase(key) continue var path = items_dict[key].texture_path if not path: var placeholder = preload("res://asset/art/ui/hud/placeholder.png") cached_inventory_textures[key] = placeholder print("Cache load prop placeholder key=", key) continue var texture = load(path) as Texture2D if texture: if GlobalConfig.DEBUG: print("Cache load prop texture key=", key) cached_inventory_textures[key] = texture # wrap index inventory.current_index = wrapi(inventory.current_index, 0, inventory.enabled_items.size()) func _update_prop_display_with_texture(): if not inventory: return var prop = prop_containers[0].get_child(0).get_child(0) as TextureButton # 在没有道具时,展示空手 placeholder if inventory.enabled_items.size() == 0: prop.texture_normal = preload("res://asset/art/ui/hud/placeholder.png") prop.size = Vector2(PROP_CONTROL_X, PROP_CONTROL_X) prop.scale = Vector2(1.0, 1.0) title_label.text = tr("prop_空手") return for i in range(prop_containers.size()): var id = wrapi(inventory.current_index + i, 0, inventory.enabled_items.size()) var item = items_dict[inventory.enabled_items[id]] prop = prop_containers[i].get_child(0).get_child(0) as TextureButton if i == 0: title_label.text = tr(item.key) if item.key in cached_inventory_textures: prop.texture_normal = cached_inventory_textures[item.key] var t_size = prop.texture_normal.get_size() var max_x = max(t_size.x, t_size.y) var p_scale = min(PROP_CONTROL_X / t_size.x, PROP_CONTROL_X / t_size.y) prop.scale = Vector2(p_scale, p_scale) prop.size = Vector2(max_x, max_x) else: printerr("PropHUD: Texture not found! key=", item.key) prop.texture_normal = null props_scroll.scroll_horizontal = PROP_CONTAINER_X func on_left_pressed() -> void: if locked: return sfx_click.play() _mouse_moved_on_listening() if inventory.index_wrap_add(-1): selected = false _update_prop_display_with_texture() _tween_container(true) func on_right_pressed() -> void: if locked: return sfx_click.play() _mouse_moved_on_listening() if inventory.index_wrap_add(1): selected = false _update_prop_display_with_texture() _tween_container(false) func _tween_container(left_to_right := true) -> void: if container_tween and container_tween.is_running(): container_tween.kill() container_tween = create_tween() if left_to_right: props_scroll.scroll_horizontal = 2 * PROP_CONTAINER_X else: props_scroll.scroll_horizontal = 0.0 # 通过 Head 与 Tail 占位符,实现滚动效果;否则会导致卡在边界上,无法滚动 container_tween.tween_property(props_scroll, "scroll_horizontal", PROP_CONTAINER_X, 0.2) func _on_prop_pressed(id := 0) -> void: if locked: return sfx_click.play() if GlobalConfig.DEBUG: print("PropHUD Panel pressed.") if not selected: focus_mode = FOCUS_ALL grab_focus() selected = true inventory.current_index += id _update_prop_display_with_texture() _mouse_moved_on_listening(true) var tween_scroll: Tween func _toggle_scroll(fold := true) -> void: if tween_scroll and tween_scroll.is_running(): tween_scroll.kill() if fold: tween_scroll = create_tween() tween_scroll.tween_property(props_scroll, "custom_minimum_size:x", PROP_CONTAINER_X, 0.5) tween_scroll.parallel().tween_property( hud_rect, "custom_minimum_size:x", PROP_CONTAINER_X + 10., 0.5 ) else: # 保持最小宽度 var x_size = max(1, prop_containers.size()) * PROP_CONTAINER_X tween_scroll = create_tween() tween_scroll.tween_property(props_scroll, "custom_minimum_size:x", x_size, 0.5) tween_scroll.parallel().tween_property(hud_rect, "custom_minimum_size:x", x_size + 10., 0.5) func _on_mouse_entered() -> void: if locked: return listen_mouse = true _mouse_moved_on_listening() func _on_mouse_exited() -> void: listen_mouse = false func _mouse_moved_on_listening(unfold_scroll := false) -> void: if unfold_scroll: _toggle_scroll(false) if not displaying: _toggle_details(true) return timer.start(display_time) func _on_timer_timeout() -> void: _toggle_details(false) _toggle_scroll(true) func _unhandled_input(event: InputEvent) -> void: if locked: return if event.is_action_pressed("prop_left"): on_left_pressed() get_viewport().set_input_as_handled() elif event.is_action_pressed("prop_right"): on_right_pressed() get_viewport().set_input_as_handled() func _input(event: InputEvent) -> void: if locked: return if listen_mouse and (event is InputEventMouseMotion or event is InputEventScreenTouch): _mouse_moved_on_listening() func _toggle_details(display := true) -> void: if display_tween and display_tween.is_running(): display_tween.kill() display_tween = create_tween() if display: displaying = true _toggle_btn_ability(true) display_tween.parallel().tween_property(title_label, "modulate:a", 1.0, 0.3) display_tween.parallel().tween_property(left_btn, "modulate:a", 1.0, 0.3) display_tween.parallel().tween_property(right_btn, "modulate:a", 1.0, 0.3) timer.start(display_time) else: displaying = false display_tween.parallel().tween_property(title_label, "modulate:a", 0.0, 0.6) display_tween.parallel().tween_property(left_btn, "modulate:a", 0.5, 0.5) display_tween.parallel().tween_property(right_btn, "modulate:a", 0.5, 0.5) # display_tween.tween_callback(_toggle_btn_ability.bind(false)) timer.stop() func _toggle_btn_ability(v: bool) -> void: left_btn.disabled = !v right_btn.disabled = !v func enable_prop_item(prop_key: String) -> void: if not inventory or not prop_key: return if not items_dict.has(prop_key): push_error("PropItem not found! key=" + prop_key) return inventory.enable_item(prop_key) _align_container_size() _load_texture_cache() _update_prop_display_with_texture() if GlobalConfig.DEBUG: print("PropHUD Enable prop item:", prop_key) var inspector = SceneManager.get_inspector() if inspector: var inspect_path = items_dict[prop_key].inspect_path var prop_title = tr(prop_key) if inspect_path: var texture = load(inspect_path) as Texture2D inspector.pop_prop_inspection(prop_title, texture) else: var texture = cached_inventory_textures[prop_key] inspector.pop_prop_inspection(prop_title, texture, true) func disable_prop_item(prop_key: String) -> void: if not inventory or not prop_key: return inventory.disable_item(prop_key) _align_container_size() _load_texture_cache() _update_prop_display_with_texture() func _align_container_size() -> void: # 判断是否需要添加新的 prop container while inventory.enabled_items.size() > prop_containers.size(): append_prop_container() # 判断是否需要删除 prop container,最少保留一个 while inventory.enabled_items.size() < prop_containers.size() and prop_containers.size() > 1: remove_prop_container() if displaying: _mouse_moved_on_listening() # 如果正在展示,则更新 scroll 长度 if displaying and props_scroll.custom_minimum_size.x > PROP_CONTAINER_X: _toggle_scroll(false) func append_prop_container() -> void: var container = CenterContainer.new() container.custom_minimum_size = Vector2(PROP_CONTAINER_X, PROP_CONTAINER_X) container.set_anchors_preset(Control.PRESET_FULL_RECT) prop_containers.append(container) var control = Control.new() control.custom_minimum_size = Vector2(PROP_CONTROL_X, PROP_CONTROL_X) control.set_anchors_preset(Control.PRESET_FULL_RECT) container.add_child(control) var prop = TextureButton.new() prop.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED control.add_child(prop) # 添加到 hbox: container -> control -> prop props_hbox.add_child(container) # 放在 Tail 占位符 之前 props_hbox.move_child(container, -2) prop.pressed.connect(_on_prop_pressed.bind(prop_containers.size() - 1)) func remove_prop_container() -> void: if prop_containers.size() <= 1: return var container = prop_containers.pop_back() props_hbox.remove_child(container) container.queue_free() var shake_tween # 使用无效道具,抖动提示 func on_toggle_invalid_prop(): if GlobalConfig.DEBUG: print("使用无效道具. current prop:", inventory.current_item_key()) if not inventory or not inventory.current_item_key(): return if shake_tween: shake_tween.kill() # 抖动效果,逐渐减弱 shake_tween = create_tween() var fps := 15.0 var duration := 0.8 var delta := 12.0 var origin_pos = Vector2.ZERO var count = int(duration * fps) var delta_t = 1.0 / fps var prop = prop_containers[0].get_child(0) prop.modulate = Color(1.0, 0.6, 0.6, 1.0) for i in range(count): var offset = Vector2(randf_range(-delta, delta), randf_range(-delta, delta)) * exp(-i) shake_tween.tween_property(prop, "position", origin_pos + offset, delta_t).set_trans( Tween.TRANS_CUBIC ) shake_tween.tween_callback(_reset_prop_modulate) func _reset_prop_modulate(): var prop = prop_containers[0].get_child(0) prop.modulate = Color(1.0, 1.0, 1.0, 1.0)