@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 # if is_node_ready(): # %Mark.visible = value # mark.modulate.a = 1.0 @onready var sfx_click = %SfxClick as Sfx @onready var left_btn = %LeftButton as TextureButton @onready var right_btn = %RightButton as TextureButton @onready var panel = %HudPanel as TextureButton @onready var prop = %Prop as TextureRect @onready var prop_container = %PropContainer as Container @onready var mark = %Mark as TextureRect @onready var title_label = %TitleLabel as Label 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 _load_items() _load_from_archive() 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) panel.pressed.connect(_on_panel_pressed) # %Mark.visible = selected # mark.modulate.a = 0.8 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 _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: continue var texture = load(path) as Texture2D if texture: if GlobalConfig.DEBUG: print("Cache load prop texture:", path) 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 if inventory.enabled_items.size() == 0: prop.texture = null return var item = items_dict[inventory.current_item_key()] if not item: prop.texture = null push_error("PropItem is null! index=" + str(inventory.current_index)) return if item.key in cached_inventory_textures: prop.texture = cached_inventory_textures[item.key] else: prop.texture = null title_label.text = tr(item.key) 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: container_tween.tween_property(prop_container, "offset_left", 0.0, 0.3).from(-200.0) prop_container.offset_right = 0.0 else: container_tween.tween_property(prop_container, "offset_right", 0.0, 0.3).from(200.0) prop_container.offset_left = 0.0 func _on_panel_pressed() -> void: if locked: return sfx_click.play() if not selected: focus_mode = FOCUS_ALL grab_focus() selected = true _mouse_moved_on_listening() 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() -> void: if not displaying: toggle_details(true) return timer.start(display_time) func _on_timer_timeout() -> void: toggle_details(false) 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() elif event.is_action_pressed("prop_select"): _on_panel_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) # display_tween.parallel().tween_property(mark, "modulate:a", 1.0, 0.3) # display_tween.parallel().tween_property(mark, "scale", Vector2(1.1, 1.1), 0.3).set_trans( # Tween.TRANS_CUBIC # ) # display_tween.tween_property(mark, "scale", Vector2.ONE, 0.2).set_trans(Tween.TRANS_CUBIC) timer.start(display_time) else: displaying = false # display_tween.tween_property(mark, "modulate:a", 0.8, 0.6) 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) _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) _load_texture_cache() _update_prop_display_with_texture() # save to archive immediately ArchiveManager.save_all() 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 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(): prop.modulate = Color(1.0, 1.0, 1.0, 1.0)