# @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(_emit_changed): inventory.current_item_changed.connect(_emit_changed) @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 := true: set(value): if selected != value: selected = value _emit_changed() @onready var sfx_click = %SfxClick as Sfx @onready var sfx_inspect = %SfxInspect as Sfx @onready var sfx_new_prop = %SfxNewProp as Sfx @onready var left_btn = %LeftButton as TextureButton @onready var right_btn = %RightButton as TextureButton @onready var title_label = %TitleLabel as Label # hud_rect 为背景, prop_scroll 为滚动区域,prop_hbox 为道具容器 @onready var hud_rect = %HudRect as NinePatchRect @onready var prop_scroll = %PropScrollContainer as ScrollContainer @onready var prop_hbox = %PropHBox as HBoxContainer @onready var display_prop = %DiaplayProp as TextureButton @onready var selecting_bg = %SelectingBG as TextureRect # hud_rect 最左侧为 prop_scroll, 最右侧为 props_bag_scroll @onready var props_bag_scroll = %PropsBagScroll as ScrollContainer @onready var props_bag = %PropsBag as HBoxContainer @onready var select_mark = %SelectMark as TextureRect var prop_containers = [] #CenterContainer const PROP_CONTAINER_X = 130.0 const PROP_CONTROL_X = 110.0 var items_dict := {} var items_description_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 := {} # hud 展示与隐藏动效 var listen_mouse = false var displaying = false var timer := Timer.new() var display_tween: Tween var container_tween: Tween # hud 是否监听快捷键 var listening_hotkey = true func _emit_changed(prop_key := ""): if not selected: current_item_changed.emit("") else: current_item_changed.emit(prop_key) func _ready() -> void: if Engine.is_editor_hint(): return # read prop containers for id in range(props_bag.get_child_count()): var container = props_bag.get_child(id) prop_containers.append(container) container.get_child(0).get_child(0).gui_input.connect(_on_prop_gui_input.bind(id)) display_prop.gui_input.connect(_on_prop_gui_input.bind(-1)) _load_items() _load_from_archive() selecting_bg.modulate.a = 0.0 prop_scroll.scroll_horizontal = PROP_CONTAINER_X props_bag_scroll.scroll_horizontal = 0.0 props_bag_scroll.custom_minimum_size.x = 0.0 # 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 _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 # # 检查是否是重要物品,如果是,则添加到 items_description_dict items_description_dict[item.key] = tr(item.key + "_说明").replace("
", "\n") # if item_config_res.titles.has(item.key): # var important_item_line_id = item_config_res.titles[item.key] # items_description_dict[item.key] = get_import_item_content(important_item_line_id) 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 get_import_item_content(line_id) -> String: # var current_line = item_config_res.lines[line_id] # var content = "" # while current_line: # if current_line.has("text"): # if current_line.has("translation_key"): # content += tr(current_line.translation_key) + "\n" # else: # content += current_line.text + "\n" # if not current_line.has("next_id") or current_line.next_id == "end": # break # current_line = item_config_res.lines[current_line.next_id] # content.replace("
", "\n") # return content 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 checkout_inventory(inv: PropInventory) -> void: inventory = inv _align_container_size() _load_texture_cache() _update_prop_display_with_texture() func hide_hud(): # 隐藏 hud 并静音 hide() listening_hotkey = false func display_hud(): # 显示 hud 并取消静音 show() listening_hotkey = true 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 key = "" # 在没有道具时,展示空手 placeholder if inventory.enabled_items.is_empty(): display_prop.texture_normal = preload("res://asset/art/ui/hud/placeholder.png") display_prop.size = Vector2(PROP_CONTROL_X, PROP_CONTROL_X) display_prop.scale = Vector2(1.0, 1.0) title_label.text = tr("prop_空手") else: key = inventory.current_item_key() _display_texture_by_key(display_prop, key) title_label.text = tr(key) # 如果当前是 prop_小猫玩具完整,尝试点亮玩家的灯效;否则无需点亮 var player = SceneManager.get_player() if player: player.set_catty_light(key == "prop_小猫玩具完整") # 选中标记 select_mark; 如果被 free 掉,则重新创建 if select_mark and is_instance_valid(select_mark): var parent = select_mark.get_parent() if parent: parent.remove_child(select_mark) else: select_mark = TextureRect.new() select_mark.custom_minimum_size = Vector2(PROP_CONTAINER_X, PROP_CONTAINER_X) select_mark.texture = preload("res://asset/art/ui/hud/select_mark.png") # bag for i in range(inventory.enabled_items.size()): var id = wrapi(i, 0, inventory.enabled_items.size()) var k = inventory.enabled_items[id] var button = prop_containers[i].get_child(0).get_child(0) as TextureButton _display_texture_by_key(button, k) if id == inventory.current_index: prop_containers[i].get_child(0).add_child(select_mark) prop_scroll.scroll_horizontal = PROP_CONTAINER_X # props_bag_scroll.scroll_horizontal = 0.0 func _display_texture_by_key(button, key) -> void: if not key: button.texture_normal = null return var item = items_dict[key] button.texture_normal = cached_inventory_textures[item.key] var t_size = button.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) button.scale = Vector2(p_scale, p_scale) button.size = Vector2(max_x, max_x) func on_left_pressed() -> void: if locked: return sfx_click.play() if inventory.index_wrap_add(-1): selected = true _update_prop_display_with_texture() _tween_container(true) _mouse_moved_on_listening() func on_right_pressed() -> void: if locked: return sfx_click.play() if inventory.index_wrap_add(1): selected = true _update_prop_display_with_texture() _tween_container(false) _mouse_moved_on_listening() 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: prop_scroll.scroll_horizontal = 2 * PROP_CONTAINER_X else: prop_scroll.scroll_horizontal = 0.0 # 通过 Head 与 Tail 占位符,实现滚动效果;否则会导致卡在边界上,无法滚动 container_tween.tween_property(prop_scroll, "scroll_horizontal", PROP_CONTAINER_X, 0.2) func _on_prop_gui_input(event: InputEvent, id) -> void: if locked: return if event is InputEventMouseButton and event.is_pressed(): event = event as InputEventMouseButton if event.button_index == MOUSE_BUTTON_LEFT: _on_prop_pressed(id) elif event.button_index == MOUSE_BUTTON_RIGHT: _on_prop_inspected(id) func _on_prop_inspected(id := 0) -> void: if locked: return var prop_key = "" if id >= 0: prop_key = inventory.enabled_items[id] else: prop_key = inventory.current_item_key() if prop_key: sfx_inspect.play() inspect_item(prop_key, true) func _on_prop_pressed(id := 0) -> void: if locked: return sfx_click.play() if GlobalConfig.DEBUG: print("PropHUD Panel pressed.") focus_mode = FOCUS_ALL grab_focus() selected = true if id >= 0: inventory.current_index = id _update_prop_display_with_texture() _mouse_moved_on_listening(true) var prop_blink: Tween var tween_scroll: Tween func _toggle_scroll(fold := true) -> void: if prop_blink and prop_blink.is_running(): prop_blink.kill() if tween_scroll and tween_scroll.is_running(): tween_scroll.kill() # PROP_CONTAINER_X + 10. 为hud的两侧留出的空间 if fold: # kill blink select_mark.modulate.a = 1.0 selecting_bg.modulate.a = 0.0 # fold tween_scroll = create_tween() tween_scroll.tween_property(props_bag_scroll, "custom_minimum_size:x", 0.0, 0.5) tween_scroll.parallel().tween_property( hud_rect, "custom_minimum_size:x", PROP_CONTAINER_X + 10., 0.5 ) else: # blink if prop_containers.size() > 0: prop_blink = create_tween().set_loops(0) prop_blink.tween_property(select_mark, "modulate:a", 0.6, 1.).set_ease( Tween.EASE_IN_OUT ) prop_blink.parallel().tween_property(selecting_bg, "modulate:a", 0.2, 1.).set_ease( Tween.EASE_IN_OUT ) prop_blink.tween_property(select_mark, "modulate:a", 1., 1.).set_ease(Tween.EASE_IN_OUT) prop_blink.parallel().tween_property(selecting_bg, "modulate:a", .6, 1.).set_ease( Tween.EASE_IN_OUT ) # unfold var bag_x = prop_containers.size() * PROP_CONTAINER_X var hud_x = bag_x + PROP_CONTAINER_X + 10. tween_scroll = create_tween() tween_scroll.tween_property(props_bag_scroll, "custom_minimum_size:x", bag_x, 0.5) tween_scroll.parallel().tween_property(hud_rect, "custom_minimum_size:x", hud_x, 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 or not listening_hotkey: 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, inspect := true) -> 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) if inspect: sfx_new_prop.play() inspect_item(prop_key) func inspect_item(prop_key: String, display_words_only := false): var inspector = SceneManager.get_inspector() if inspector: var inspect_path = items_dict[prop_key].inspect_path # 是否有独立的 inspect 图片 if inspect_path: var texture = load(inspect_path) as Texture2D inspector.pop_prop_inspection(prop_key, texture, false, display_words_only) else: var texture = cached_inventory_textures[prop_key] inspector.pop_prop_inspection(prop_key, texture, true, display_words_only) 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(): remove_prop_container() if displaying: _mouse_moved_on_listening() # 如果正在展示,则更新 scroll 长度 if displaying and props_bag_scroll.custom_minimum_size.x > 0.0: _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_bag.add_child(container) prop.gui_input.connect(_on_prop_gui_input.bind(prop_containers.size() - 1)) func remove_prop_container() -> void: if prop_containers.size() == 0: return var container = prop_containers.pop_back() props_bag.remove_child(container) container.queue_free() var shake_tween # 使用无效道具,抖动提示 func on_toggle_invalid_prop(): if not inventory or not inventory.current_item_key(): return if GlobalConfig.DEBUG: print("使用无效道具. invalid_prop shake. current prop:", inventory.current_item_key()) 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 = display_prop # 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 = display_prop # var prop = prop_containers[0].get_child(0) prop.modulate = Color(1.0, 1.0, 1.0, 1.0)