xiandie/scene/prop/prop_hud.gd

478 lines
15 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)
@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 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 := {}
# 从配置文件加载 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 _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).pressed.connect(_on_prop_pressed.bind(id))
display_prop.pressed.connect(_on_prop_pressed.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
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
# 在没有道具时,展示空手 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:
var key = inventory.current_item_key()
_display_texture_by_key(display_prop, key)
title_label.text = tr(key)
# 选中标记 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 key = inventory.enabled_items[id]
var button = prop_containers[i].get_child(0).get_child(0) as TextureButton
_display_texture_by_key(button, key)
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_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:
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():
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.pressed.connect(_on_prop_pressed.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 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)