xiandie/scene/ux/prop_hud.gd

680 lines
19 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @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
const HUD_FADE_DURATION = 0.3
@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 hud_visibility_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()
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