feat: complete mobile UI implementation

This commit is contained in:
shenlei
2026-09-04 19:54:33 +09:00
parent 9a4a044da5
commit b19d5b5dd3
132 changed files with 6643 additions and 180 deletions
+88 -8
View File
@@ -17,6 +17,8 @@ const PlayerCtl = preload("res://player_controller.gd")
const NetWorld = preload("res://net_world.gd")
const NetPlay = preload("res://net_play.gd")
const Hud = preload("res://hud.gd")
const UiProfile = preload("res://ui/ui_profile.gd")
const MobileUiRoot = preload("res://ui/mobile/mobile_ui_root.gd")
const Audio = preload("res://audio.gd")
const BgmDirector = preload("res://bgm_director.gd")
const UiManager = preload("res://ui/ui_manager.gd")
@@ -134,11 +136,13 @@ var _guild_invite_dialog: ConfirmationDialog
var _gift_dialog: AcceptDialog
var _quick_page_mode := false
var _show_names := false
var _ui_mobile := false
func setup(m2client: Node, assets_root: String,
initial_map_path: String = "OutdoorA1/metin2_map_a1") -> void:
client = m2client
_assets = assets_root
_ui_mobile = UiProfile.resolve() == UiProfile.Mode.MOBILE
# GC_MAIN_CHARACTER only carries the server-space coordinates, not the map
# folder. The old fixed A1 fallback puts characters from A2/A3 outside the
# rendered terrain, which looks like an empty map. Resolve the map from the
@@ -187,12 +191,17 @@ func setup(m2client: Node, assets_root: String,
or int(pickable.get_meta("vid", -1)) == vid:
pc.pickables.erase(pickable))
hud = Hud.new()
if _map_loaded():
hud.setup(world, player) # 含小地图 / 快捷栏 / 背包
else:
if _ui_mobile:
hud = MobileUiRoot.new()
add_child(hud)
hud.call("_build_status") # 无地图兜底:只搭状态栏(血 / 蓝 / 经验 / 等级)
hud.setup(world, player, client)
else:
hud = Hud.new()
add_child(hud)
if _map_loaded():
hud.setup(world, player) # 含小地图 / 快捷栏 / 背包
else:
hud.call("_build_status") # 无地图兜底:只搭状态栏(血 / 蓝 / 经验 / 等级)
net_play = NetPlay.new()
add_child(net_play)
@@ -541,6 +550,9 @@ func setup(m2client: Node, assets_root: String,
if client.has_signal("channel_changed") and hud and hud.has_method("set_channel"):
client.channel_changed.connect(func(c): hud.set_channel(c))
if _ui_mobile and hud and hud.has_method("bind_controls"):
_setup_mobile_ui()
client.entity_main_set.connect(_on_main_set)
if client.get_main_vid() != 0: # 重连 / 已在局内
# net_play._on_main_set 的信号早在它连上前就发过了 —— 手动补上关键那步:
@@ -558,8 +570,50 @@ func setup(m2client: Node, assets_root: String,
net_world.catch_up()
set_process_unhandled_input(true)
func _setup_mobile_ui() -> void:
# MobileUiRoot owns the presentation and touch layer; every feature node in
# this dictionary remains the existing PC/business controller.
hud.bind_controls({
"client": client,
"ui": ui,
"lifecycle": _lifecycle_node(),
"player_controller": pc,
"net_play": net_play,
"quickbar": quickbar,
"ground_items": ground_items,
"inventory": inventory,
"atlas_ui": atlas_ui,
"quest_log": quest_log,
"party_ui": party_ui,
"char_status_ui": char_status_ui,
"skills": skills,
"chat": chat,
"friend_ui": friend_ui,
"guild_ui": guild_ui,
"love_ui": love_ui,
"refine_ui": refine_ui,
"cube_ui": cube_ui,
"mall_ui": mall_ui,
"shop_ui": shop_ui,
"exchange_ui": exchange_ui,
"safebox_ui": safebox_ui,
"private_shop_ui": private_shop_ui,
"system_menu_ui": system_menu_ui,
"system_option_ui": system_option_ui,
"game_option_ui": game_option_ui,
"minimap": minimap,
})
set_process(true)
func _lifecycle_node() -> Node:
# AppFlow owns the single lifecycle observer across login/select/game. The
# game scene only borrows it for Android back and focus recovery.
var owner := get_parent()
if owner:
return owner.get_node_or_null("AppLifecycle")
return null
func _process(_dt: float) -> void:
# MilesLib::CSoundManager::SetPosition/SetDirection + Update. The audio
# node keeps its listener at the origin and rebases active 3D instances.
@@ -591,14 +645,17 @@ func _on_guild_invite(guild_id: int, guild_name: String) -> void:
_guild_invite_dialog.dialog_text = "加入公会“%s”?" % guild_name
_guild_invite_dialog.ok_button_text = "加入"
_guild_invite_dialog.cancel_button_text = "拒绝"
_guild_invite_dialog.set_meta("mobile_title", "公会邀请")
ui.add_child(_guild_invite_dialog)
if ui.has_method("track_mobile_modal"):
ui.track_mobile_modal(_guild_invite_dialog)
_guild_invite_dialog.confirmed.connect(func():
client.guild_answer_invite(guild_id, true)
_guild_invite_dialog.queue_free())
_guild_invite_dialog.canceled.connect(func():
client.guild_answer_invite(guild_id, false)
_guild_invite_dialog.queue_free())
_guild_invite_dialog.popup_centered()
_popup_guild_invite()
func _on_gift_available() -> void:
# 40250's Gift_Show has no corresponding claim packet in the shipped
@@ -607,17 +664,40 @@ func _on_gift_available() -> void:
if ui == null:
return
if _gift_dialog and is_instance_valid(_gift_dialog):
_gift_dialog.popup_centered()
_popup_gift()
return
_gift_dialog = AcceptDialog.new()
_gift_dialog.title = "礼物"
_gift_dialog.dialog_text = "有新的礼物可查看。"
_gift_dialog.ok_button_text = "知道了"
_gift_dialog.set_meta("mobile_title", "礼物")
ui.add_child(_gift_dialog)
if ui.has_method("track_mobile_modal"):
ui.track_mobile_modal(_gift_dialog)
_gift_dialog.confirmed.connect(func():
if is_instance_valid(_gift_dialog):
_gift_dialog.queue_free())
_gift_dialog.popup_centered()
_popup_gift()
func _popup_guild_invite() -> void:
if not is_instance_valid(_guild_invite_dialog):
return
if _ui_mobile:
_guild_invite_dialog.min_size = Vector2i(500, 230)
_guild_invite_dialog.size = Vector2i(500, 230)
_guild_invite_dialog.popup_centered(Vector2i(500, 230))
else:
_guild_invite_dialog.popup_centered()
func _popup_gift() -> void:
if not is_instance_valid(_gift_dialog):
return
if _ui_mobile:
_gift_dialog.min_size = Vector2i(460, 210)
_gift_dialog.size = Vector2i(460, 210)
_gift_dialog.popup_centered(Vector2i(460, 210))
else:
_gift_dialog.popup_centered()
# .msa MotionEventData 分派(对齐 GameLib/RaceMotionData EMotionEventType):
# 1 EFFECT / 10 EFFECT_TO_TARGET → 挂特效;2 SCREEN_WAVING → 震屏;