478 lines
15 KiB
GDScript
478 lines
15 KiB
GDScript
# @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)
|