343 lines
11 KiB
GDScript
343 lines
11 KiB
GDScript
# MobileHud —— landscape in-game HUD.
|
|
#
|
|
# This is deliberately a presentation-only node. NetPlay updates it through
|
|
# the same setter contract as hud.gd, while MobileUiRoot wires its buttons to
|
|
# the existing feature controllers.
|
|
extends Control
|
|
|
|
const TouchButton := preload("res://ui/mobile/mobile_touch_button.gd")
|
|
|
|
signal avatar_pressed
|
|
signal inventory_pressed
|
|
signal minimap_pressed
|
|
signal menu_pressed
|
|
signal quest_pressed
|
|
signal party_member_pressed(vid: int)
|
|
signal context_action_pressed
|
|
|
|
const AVATAR_SIZE := 78.0
|
|
const SAFE_MARGIN := 16.0
|
|
|
|
var client: Node
|
|
var _player_name: Label
|
|
var _avatar: TouchButton
|
|
var _hp_bar: ProgressBar
|
|
var _mp_bar: ProgressBar
|
|
var _hp_text: Label
|
|
var _mp_text: Label
|
|
var _level: Label
|
|
var _party_host: VBoxContainer
|
|
var _target_panel: Panel
|
|
var _target_name: Label
|
|
var _target_bar: ProgressBar
|
|
var _quest: TouchButton
|
|
var _context: TouchButton
|
|
var _mini_host: Control
|
|
var _mini_placeholder: Label
|
|
var _channel: Label
|
|
var _affects: HBoxContainer
|
|
var _client_bound := false
|
|
|
|
func setup() -> void:
|
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_build_player_card()
|
|
_build_top_right()
|
|
_build_target()
|
|
_build_quest()
|
|
_build_context_action()
|
|
|
|
func bind_client(m2client: Node) -> void:
|
|
if _client_bound and client == m2client:
|
|
_refresh_player()
|
|
_refresh_party()
|
|
return
|
|
client = m2client
|
|
_client_bound = client != null
|
|
if client and client.has_signal("party_changed"):
|
|
client.party_changed.connect(_refresh_party)
|
|
if client and client.has_signal("vitals_changed"):
|
|
client.vitals_changed.connect(func(_vid): _refresh_party())
|
|
_refresh_player()
|
|
_refresh_party()
|
|
|
|
func set_player_name(value: String) -> void:
|
|
if _player_name:
|
|
_player_name.text = value if value != "" else "冒险者"
|
|
|
|
func get_minimap_host() -> Control:
|
|
return _mini_host
|
|
|
|
func get_quest_button() -> Control:
|
|
return _quest
|
|
|
|
func get_context_button() -> Control:
|
|
return _context
|
|
|
|
func set_vitals(hp: int, max_hp: int, sp: int, max_sp: int) -> void:
|
|
if _hp_bar:
|
|
_hp_bar.max_value = maxi(1, max_hp)
|
|
_hp_bar.value = clampi(hp, 0, maxi(1, max_hp))
|
|
if _mp_bar:
|
|
_mp_bar.max_value = maxi(1, max_sp)
|
|
_mp_bar.value = clampi(sp, 0, maxi(1, max_sp))
|
|
if _hp_text:
|
|
_hp_text.text = "%d/%d" % [hp, max_hp]
|
|
if _mp_text:
|
|
_mp_text.text = "%d/%d" % [sp, max_sp]
|
|
|
|
func set_exp(xp: int, next_xp: int) -> void:
|
|
# The mobile card keeps the main HUD quiet; experience is shown in the
|
|
# level caption so it remains available without adding a bottom bar.
|
|
if _level and next_xp > 0:
|
|
_level.tooltip_text = "经验 %d / %d" % [xp, next_xp]
|
|
|
|
func set_level(value: int) -> void:
|
|
if _level:
|
|
_level.text = "Lv %d" % value
|
|
|
|
func set_energy(value: int, max_value: int = 100) -> void:
|
|
if _avatar:
|
|
_avatar.tooltip_text = "职业资源 %d/%d" % [value, max_value]
|
|
|
|
func set_stamina(value: int, max_value: int) -> void:
|
|
if _avatar:
|
|
_avatar.tooltip_text = "体力 %d/%d" % [value, max_value]
|
|
|
|
func set_affects(values: Array) -> void:
|
|
if _affects == null:
|
|
return
|
|
for child in _affects.get_children():
|
|
child.queue_free()
|
|
for value in values:
|
|
var chip := Label.new()
|
|
chip.text = "◆"
|
|
chip.add_theme_font_size_override("font_size", 11)
|
|
chip.add_theme_color_override("font_color", Color.from_hsv(
|
|
fmod(float(int(value.get("type", 0))) * 0.13, 1.0), 0.55, 0.95))
|
|
chip.tooltip_text = "状态 #%d" % int(value.get("type", 0))
|
|
_affects.add_child(chip)
|
|
|
|
func set_target(name: String, hp_pct: int) -> void:
|
|
if _target_panel == null:
|
|
_build_target()
|
|
_target_panel.visible = true
|
|
_target_name.text = name if name != "" else "目标"
|
|
_target_bar.value = clampi(hp_pct, 0, 100)
|
|
|
|
func clear_target() -> void:
|
|
if _target_panel:
|
|
_target_panel.visible = false
|
|
|
|
func set_channel(value: int) -> void:
|
|
if _channel:
|
|
_channel.text = "CH %d" % value
|
|
|
|
func set_dungeon_destination(active: bool, _world_pos: Vector3) -> void:
|
|
if _quest and active:
|
|
_quest.set_caption("副本目标")
|
|
|
|
func set_context_action(text: String, visible: bool) -> void:
|
|
if _context == null:
|
|
return
|
|
_context.set_caption(text)
|
|
_context.visible = visible
|
|
|
|
func _build_player_card() -> void:
|
|
var card := Control.new()
|
|
card.position = Vector2(SAFE_MARGIN, SAFE_MARGIN)
|
|
card.size = Vector2(110, 122)
|
|
add_child(card)
|
|
_player_name = Label.new()
|
|
_player_name.text = "冒险者"
|
|
_player_name.position = Vector2(0, 0)
|
|
_player_name.size = Vector2(AVATAR_SIZE + 10, 20)
|
|
_player_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_player_name.add_theme_font_size_override("font_size", 13)
|
|
_player_name.add_theme_color_override("font_color", Color(0.98, 0.88, 0.61))
|
|
_player_name.add_theme_color_override("font_outline_color", Color(0.01, 0.02, 0.04, 0.95))
|
|
_player_name.add_theme_constant_override("outline_size", 4)
|
|
card.add_child(_player_name)
|
|
|
|
_avatar = TouchButton.new()
|
|
_avatar.setup("角\n色", Color(0.92, 0.72, 0.32, 0.95))
|
|
_avatar.position = Vector2(5, 20)
|
|
_avatar.size = Vector2(AVATAR_SIZE, AVATAR_SIZE)
|
|
_avatar.pressed.connect(func(): avatar_pressed.emit())
|
|
card.add_child(_avatar)
|
|
|
|
_hp_bar = _bar(Color(0.86, 0.24, 0.22), Vector2(AVATAR_SIZE, 9))
|
|
_hp_bar.position = Vector2(5, 100)
|
|
card.add_child(_hp_bar)
|
|
_hp_text = _bar_text(_hp_bar)
|
|
|
|
_mp_bar = _bar(Color(0.24, 0.48, 0.93), Vector2(AVATAR_SIZE, 7))
|
|
_mp_bar.position = Vector2(5, 111)
|
|
card.add_child(_mp_bar)
|
|
_mp_text = _bar_text(_mp_bar)
|
|
|
|
_level = Label.new()
|
|
_level.text = "Lv 1"
|
|
_level.position = Vector2(86, 27)
|
|
_level.add_theme_font_size_override("font_size", 11)
|
|
_level.add_theme_color_override("font_color", Color(0.78, 0.86, 0.98))
|
|
card.add_child(_level)
|
|
|
|
_party_host = VBoxContainer.new()
|
|
_party_host.position = Vector2(0, 126)
|
|
_party_host.size = Vector2(196, 180)
|
|
_party_host.add_theme_constant_override("separation", 4)
|
|
add_child(_party_host)
|
|
|
|
_affects = HBoxContainer.new()
|
|
_affects.position = Vector2(5, 121)
|
|
_affects.add_theme_constant_override("separation", 3)
|
|
card.add_child(_affects)
|
|
|
|
func _build_top_right() -> void:
|
|
var actions := Control.new()
|
|
actions.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
|
actions.position = Vector2(-250, SAFE_MARGIN)
|
|
actions.size = Vector2(234, 98)
|
|
add_child(actions)
|
|
|
|
var bag := TouchButton.new()
|
|
bag.setup("背包", Color(0.95, 0.72, 0.34, 0.95))
|
|
bag.position = Vector2(0, 18)
|
|
bag.size = Vector2(56, 56)
|
|
bag.pressed.connect(func(): inventory_pressed.emit())
|
|
actions.add_child(bag)
|
|
|
|
_mini_host = Control.new()
|
|
_mini_host.position = Vector2(62, 0)
|
|
_mini_host.size = Vector2(96, 96)
|
|
_mini_host.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
actions.add_child(_mini_host)
|
|
_mini_placeholder = Label.new()
|
|
_mini_placeholder.text = "地图"
|
|
_mini_placeholder.position = Vector2(28, 38)
|
|
_mini_placeholder.add_theme_font_size_override("font_size", 12)
|
|
_mini_placeholder.add_theme_color_override("font_color", Color(0.8, 0.9, 1.0, 0.85))
|
|
_mini_placeholder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_mini_host.add_child(_mini_placeholder)
|
|
|
|
var map_button := TouchButton.new()
|
|
map_button.setup("", Color(0.45, 0.7, 0.95, 0.95))
|
|
map_button.position = Vector2(62, 0)
|
|
map_button.size = Vector2(96, 96)
|
|
map_button.set_idle_modulate(Color(1, 1, 1, 0.03))
|
|
map_button.pressed.connect(func(): minimap_pressed.emit())
|
|
actions.add_child(map_button)
|
|
|
|
var menu := TouchButton.new()
|
|
menu.setup("菜单", Color(0.58, 0.74, 0.96, 0.95))
|
|
menu.position = Vector2(164, 18)
|
|
menu.size = Vector2(56, 56)
|
|
menu.pressed.connect(func(): menu_pressed.emit())
|
|
actions.add_child(menu)
|
|
|
|
_channel = Label.new()
|
|
_channel.text = "CH -"
|
|
_channel.position = Vector2(75, 98)
|
|
_channel.add_theme_font_size_override("font_size", 10)
|
|
_channel.add_theme_color_override("font_color", Color(0.64, 0.75, 0.9))
|
|
actions.add_child(_channel)
|
|
|
|
func _build_target() -> void:
|
|
_target_panel = Panel.new()
|
|
_target_panel.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
|
_target_panel.position = Vector2(-112, 14)
|
|
_target_panel.size = Vector2(224, 48)
|
|
_target_panel.visible = false
|
|
_target_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_target_panel.add_theme_stylebox_override("panel", _panel_style(Color(0.12, 0.05, 0.06, 0.84), Color(0.82, 0.3, 0.3)))
|
|
add_child(_target_panel)
|
|
_target_name = Label.new()
|
|
_target_name.position = Vector2(10, 4)
|
|
_target_name.size = Vector2(204, 18)
|
|
_target_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_target_name.add_theme_font_size_override("font_size", 12)
|
|
_target_panel.add_child(_target_name)
|
|
_target_bar = _bar(Color(0.88, 0.24, 0.23), Vector2(204, 9))
|
|
_target_bar.position = Vector2(10, 28)
|
|
_target_bar.max_value = 100
|
|
_target_bar.value = 100
|
|
_target_panel.add_child(_target_bar)
|
|
|
|
func _build_quest() -> void:
|
|
_quest = TouchButton.new()
|
|
_quest.setup("任务 · 点击查看", Color(1.0, 0.72, 0.3, 0.9))
|
|
_quest.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
|
_quest.position = Vector2(-244, 126)
|
|
_quest.size = Vector2(224, 45)
|
|
_quest.pressed.connect(func(): quest_pressed.emit())
|
|
add_child(_quest)
|
|
|
|
func _build_context_action() -> void:
|
|
_context = TouchButton.new()
|
|
_context.setup("交互", Color(0.92, 0.68, 0.31, 0.95))
|
|
_context.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
|
_context.position = Vector2(110, -116)
|
|
_context.size = Vector2(74, 48)
|
|
_context.visible = false
|
|
_context.pressed.connect(func(): context_action_pressed.emit())
|
|
add_child(_context)
|
|
|
|
func _refresh_player() -> void:
|
|
if client == null or not client.has_method("get_main_vid") or not client.has_method("get_entity"):
|
|
return
|
|
var entity: Dictionary = client.get_entity(client.get_main_vid())
|
|
set_player_name(String(entity.get("name", "冒险者")))
|
|
set_level(int(entity.get("level", 1)))
|
|
|
|
func _refresh_party() -> void:
|
|
if _party_host == null:
|
|
return
|
|
for child in _party_host.get_children():
|
|
child.queue_free()
|
|
if client == null or not client.has_method("get_party"):
|
|
return
|
|
var members: Array = client.get_party()
|
|
for member in members:
|
|
var vid := int(member.get("vid", 0))
|
|
var row := TouchButton.new()
|
|
row.setup(" %s %d%%" % [String(member.get("name", "队员")), int(member.get("hp_pct", 0))],
|
|
Color(0.32, 0.52, 0.76, 0.8))
|
|
row.set_rect_style()
|
|
row.custom_minimum_size = Vector2(184, 29)
|
|
row.size = Vector2(184, 29)
|
|
row.pressed.connect(func(): party_member_pressed.emit(vid))
|
|
_party_host.add_child(row)
|
|
|
|
func _bar(color: Color, bar_size: Vector2) -> ProgressBar:
|
|
var bar := ProgressBar.new()
|
|
bar.custom_minimum_size = bar_size
|
|
bar.size = bar_size
|
|
bar.min_value = 0
|
|
bar.max_value = 100
|
|
bar.value = 100
|
|
bar.show_percentage = false
|
|
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
bar.add_theme_stylebox_override("background", _panel_style(Color(0.01, 0.02, 0.04, 0.78), Color(0.1, 0.14, 0.2, 0.6), 2))
|
|
bar.add_theme_stylebox_override("fill", _panel_style(color, color, 2))
|
|
return bar
|
|
|
|
func _bar_text(bar: ProgressBar) -> Label:
|
|
var label := Label.new()
|
|
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
label.add_theme_font_size_override("font_size", 8)
|
|
label.add_theme_color_override("font_color", Color(0.96, 0.98, 1.0))
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
bar.add_child(label)
|
|
return label
|
|
|
|
func _panel_style(bg: Color, border: Color, width := 1) -> StyleBoxFlat:
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = bg
|
|
style.border_color = border
|
|
style.set_border_width_all(width)
|
|
style.set_corner_radius_all(6)
|
|
return style
|