# @tool class_name PropHud extends Control signal current_item_changed(prop_key: String) # 常量定义 const PROP_CONTAINER_X = 130.0 const PROP_CONTROL_X = 110.0 const TWEEN_DURATION = 0.5 const SHAKE_FPS = 15.0 const SHAKE_DURATION = 0.8 const SHAKE_DELTA = 12.0 @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): if inventory == value: return if inventory and inventory.current_item_changed.is_connected(_emit_changed): inventory.current_item_changed.disconnect(_emit_changed) 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 @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: Array[CenterContainer] = [] var items_dict := {} var items_description_dict = {} # 从配置文件加载 prop items var item_config_res = preload("uid://b1vwhxctfhl5d") #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 var prop_blink: Tween var tween_scroll: Tween var shake_tween: Tween # hud 是否监听快捷键 var listening_hotkey = true # 缓存的占位符纹理 var placeholder_texture: Texture2D func _emit_changed(prop_key := ""): current_item_changed.emit(prop_key) func _ready() -> void: if Engine.is_editor_hint(): return # 初始化占位符纹理 placeholder.png placeholder_texture = preload("uid://djrfdhywg7uu2") # read prop containers for id in range(props_bag.get_child_count()): var container = props_bag.get_child(id) prop_containers.append(container) var button = container.get_child(0).get_child(0) if button: button.gui_input.connect(_on_prop_gui_input.bind(id)) display_prop.gui_input.connect(_on_prop_gui_input.bind(-1)) _load_items_config_to_dict("ImportantPropItems") _load_items_config_to_dict("PropItems") _reload_cache_and_realign_display() 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 # 存档更新时,从存档加载 prop ArchiveManager.archive_loaded.connect(_reload_cache_and_realign_display) # 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 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_config_to_dict(title: String): if not item_config_res or not item_config_res.titles.has(title): return var id = item_config_res.titles[title] var current_line = item_config_res.lines.get(id) while current_line: if current_line.has("tags") and current_line.has("translation_key"): var texture_path = "" var inspect_path = "" for tag in current_line.tags: if tag.begins_with("texture="): texture_path = tag.substr(8).strip_edges() elif tag.begins_with("inspect="): inspect_path = tag.substr(8).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[item.key] = tr(item.key + "_说明").replace("{br}", "\n") if not current_line.has("next_id") or current_line.next_id == "end": break current_line = item_config_res.lines.get(current_line.next_id) func _reload_cache_and_realign_display() -> void: if ArchiveManager.archive: inventory = ArchiveManager.archive.prop_inventory _load_texture_cache() _align_container_size() _update_prop_display_with_texture() func checkout_inventory(character: String) -> void: if not inventory: printerr("PropHud checkout_inventory: No inventory found.") return inventory.checkout(character) _reload_cache_and_realign_display() const HUD_FADE_DURATION = 0.3 # 在变量定义区域添加 var hud_visibility_tween: Tween func hide_hud(duration: float = HUD_FADE_DURATION): if not visible: return listening_hotkey = false # 清理之前的补间动画 if hud_visibility_tween and hud_visibility_tween.is_running(): hud_visibility_tween.kill() # 创建渐变动画 hud_visibility_tween = create_tween() hud_visibility_tween.tween_property(self, "modulate:a", 0.0, duration) hud_visibility_tween.tween_callback(hide) func display_hud(duration: float = HUD_FADE_DURATION): if visible and modulate.a >= 1.0: return # 清理之前的补间动画 if hud_visibility_tween and hud_visibility_tween.is_running(): hud_visibility_tween.kill() # 确保节点可见但透明 if not visible: modulate.a = 0.0 show() # 创建渐变动画 hud_visibility_tween = create_tween() hud_visibility_tween.tween_property(self, "modulate:a", 1.0, duration) hud_visibility_tween.tween_callback(func(): listening_hotkey = true) func _load_texture_cache() -> void: if not inventory: return # 移除无效纹理 var keys_to_remove = [] for k in cached_inventory_textures: if not k in inventory.enabled_items: keys_to_remove.append(k) for k in keys_to_remove: cached_inventory_textures.erase(k) # 加载新纹理 for key in inventory.enabled_items: if cached_inventory_textures.has(key): continue if not items_dict.has(key): inventory.disable_item(key) printerr( "PropHud _load_texture_cache: key not found in items_dict:", key, ". remove item." ) continue var path = items_dict[key].texture_path if not path: cached_inventory_textures[key] = placeholder_texture 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 if inventory.enabled_items.size() > 0: 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 = placeholder_texture 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_小猫玩具完整") if GlobalConfig.DEBUG: print("[PropHud] current display prop:", key) # 选中标记处理 _update_select_mark() # 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 container = prop_containers[i] var button = container.get_child(0).get_child(0) as TextureButton _display_texture_by_key(button, k) if id == inventory.current_index: container.get_child(0).add_child(select_mark) prop_scroll.scroll_horizontal = PROP_CONTAINER_X func _update_select_mark(): if not is_instance_valid(select_mark): select_mark = TextureRect.new() select_mark.custom_minimum_size = Vector2(PROP_CONTAINER_X, PROP_CONTAINER_X) # select_mark select_mark.texture = preload("uid://c0gjes4a8ou3b") else: var parent = select_mark.get_parent() if parent: parent.remove_child(select_mark) func _display_texture_by_key(button: TextureButton, key: String) -> void: if not key or not button: if button: button.texture_normal = null return if not items_dict.has(key): return var item = items_dict[key] var texture = cached_inventory_textures.get(item.key) if not texture: if item.texture_path: texture = load(item.texture_path) as Texture2D if not texture: printerr("PropHud _display_texture_by_key: No texture found for item:", item) return else: cached_inventory_textures[item.key] = texture button.texture_normal = texture var t_size = texture.get_size() var max_dimension = 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_dimension, max_dimension) func on_left_pressed() -> void: if locked: return sfx_click.play() inventory.index_wrap_add(-1) _update_prop_display_with_texture() _tween_container(true) _mouse_moved_on_listening() func on_right_pressed() -> void: if locked: return sfx_click.play() inventory.index_wrap_add(1) _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() prop_scroll.scroll_horizontal = 2 * PROP_CONTAINER_X if left_to_right else 0.0 container_tween.tween_property(prop_scroll, "scroll_horizontal", PROP_CONTAINER_X, 0.2) func _on_prop_gui_input(event: InputEvent, id: int) -> void: if locked or not event is InputEventMouseButton: return var mouse_event = event as InputEventMouseButton if not mouse_event.is_pressed(): return match mouse_event.button_index: MOUSE_BUTTON_LEFT: _on_prop_pressed(id) MOUSE_BUTTON_RIGHT: _on_prop_inspected(id) func _on_prop_inspected(id := 0) -> void: if locked: return var prop_key = "" if id >= 0 and id < inventory.enabled_items.size(): prop_key = inventory.enabled_items[id] else: prop_key = inventory.current_item_key() if prop_key: sfx_inspect.play() print("[PropHud] inspect prop:", prop_key) inspect_item(prop_key, false) 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() if id >= 0 and id < inventory.enabled_items.size(): inventory.current_index = id _update_prop_display_with_texture() _mouse_moved_on_listening(true) 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() 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, TWEEN_DURATION) tween_scroll.parallel().tween_property( hud_rect, "custom_minimum_size:x", PROP_CONTAINER_X + 10., TWEEN_DURATION ) 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, TWEEN_DURATION ) tween_scroll.parallel().tween_property( hud_rect, "custom_minimum_size:x", hud_x, TWEEN_DURATION ) 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() elif event is InputEventMouseButton: var mouse_event = event as InputEventMouseButton if mouse_event.pressed: match mouse_event.button_index: MOUSE_BUTTON_WHEEL_DOWN: on_left_pressed() get_viewport().set_input_as_handled() MOUSE_BUTTON_WHEEL_UP: 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) timer.stop() func _toggle_btn_ability(v: bool) -> void: left_btn.disabled = !v right_btn.disabled = !v func enable_important_item(prop_key: String, display_inspector: bool) -> void: if not inventory or not prop_key: return if not items_dict.has(prop_key): push_error("ImportantPropItem not found! key=" + prop_key) return inventory.enable_important_item(prop_key) SceneManager.pop_notification("ui_notify_important_item_update") if display_inspector: sfx_inspect.play() inspect_item(prop_key, true, true) func enable_prop_item(prop_key: String, inspect := true) -> void: if not inventory or not prop_key: printerr("PropHUD Enable prop item: No inventory or prop_key provided.") return if not items_dict.has(prop_key): push_error("PropItem not found! key=" + prop_key) return inventory.enable_item(prop_key) _reload_cache_and_realign_display() 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_obtained := true, as_important_item := false): var inspector = SceneManager.get_inspector() as PropInspector if not inspector or not items_dict.has(prop_key): return var item = items_dict[prop_key] var texture: Texture2D = null # 检查是否有独立的 inspect 图片 if item.inspect_path: texture = load(item.inspect_path) as Texture2D else: texture = cached_inventory_textures.get(prop_key) if not texture and item.texture_path: texture = load(item.texture_path) as Texture2D if texture: cached_inventory_textures[prop_key] = texture if not texture: printerr("prophud inspect_item invalid texture for key:", prop_key) return inspector.pop_prop_inspection( prop_key, texture, display_obtained, as_important_item ) func disable_prop_item(prop_key: String) -> void: if not inventory or not prop_key: return inventory.disable_item(prop_key) _reload_cache_and_realign_display() func _align_container_size() -> void: if not inventory: return # 添加新容器 while inventory.enabled_items.size() > prop_containers.size(): append_prop_container() # 删除多余容器 while inventory.enabled_items.size() < prop_containers.size(): remove_prop_container() if displaying: _mouse_moved_on_listening() # 如果正在展示,则更新 scroll 长度 if 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) 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) props_bag.add_child(container) prop_containers.append(container) var index = prop_containers.size() - 1 prop.gui_input.connect(_on_prop_gui_input.bind(index)) func remove_prop_container() -> void: if prop_containers.is_empty(): return var container = prop_containers.pop_back() if container and is_instance_valid(container): props_bag.remove_child(container) container.queue_free() 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 and shake_tween.is_running(): shake_tween.kill() # 抖动效果 shake_tween = create_tween() var count = int(SHAKE_DURATION * SHAKE_FPS) var delta_t = 1.0 / SHAKE_FPS var origin_pos = Vector2.ZERO var prop = display_prop prop.modulate = Color(1.0, 0.6, 0.6, 1.0) for i in range(count): var decay = exp(-i) var offset = Vector2( randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay, randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay ) 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(): if display_prop: display_prop.modulate = Color.WHITE