# 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