feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
+223
-37
@@ -64,6 +64,7 @@ const AtlasUI = preload("res://ui/atlas_ui.gd")
|
||||
const DungeonState = preload("res://dungeon_state.gd")
|
||||
const WorldTime = preload("res://world/world_time.gd")
|
||||
const Weather = preload("res://fx/weather.gd")
|
||||
const QuestCurtain = preload("res://ui/quest_curtain.gd")
|
||||
|
||||
# 龙魂是协议兼容层的一部分,但龙魂 40250 服务器没有这个功能。
|
||||
# 保留 DragonSoulUI / ds_refine 代码供后续兼容,当前客户端不创建入口、不消费背包右键。
|
||||
@@ -78,6 +79,7 @@ var net_world: Node
|
||||
var net_play: Node
|
||||
var hud: Node
|
||||
var ui: CanvasLayer # UiManager
|
||||
var quest_curtain: QuestCurtain
|
||||
var cursor_manager: Node
|
||||
var mouse_controller: Node
|
||||
var proto: Node # Metin2Proto
|
||||
@@ -129,6 +131,7 @@ var _main_attempts := 0
|
||||
var _main_retry_at := 0
|
||||
var _main_placed_vid := 0
|
||||
var _main_map_vid := 0
|
||||
var _setup_completed := false
|
||||
const MAIN_MODEL_MAX_ATTEMPTS := 3
|
||||
|
||||
# 实机「灰紫虚空 + 蓝胶囊」的取证钩子。离线自检(package_render_test.gd)在导出包里
|
||||
@@ -194,6 +197,7 @@ func setup(m2client: Node, assets_root: String,
|
||||
pc.player = player
|
||||
pc.camera = cam
|
||||
pc.world = world
|
||||
pc.active = false
|
||||
add_child(pc)
|
||||
|
||||
_mount = Node3D.new()
|
||||
@@ -210,6 +214,8 @@ func setup(m2client: Node, assets_root: String,
|
||||
net_world.set_local_node(player)
|
||||
net_world.entity_added.connect(_on_pickable_added)
|
||||
net_world.entity_removed.connect(_on_pickable_removed)
|
||||
if client.has_signal("entity_dead"):
|
||||
client.entity_dead.connect(func(vid: int) -> void: _drop_stale_pickables(vid))
|
||||
|
||||
if _ui_mobile:
|
||||
hud = MobileUiRoot.new()
|
||||
@@ -225,7 +231,7 @@ func setup(m2client: Node, assets_root: String,
|
||||
|
||||
net_play = NetPlay.new()
|
||||
add_child(net_play)
|
||||
net_play.setup(client, pc, net_world, hud)
|
||||
net_play.setup(client, pc, net_world, hud, _audio)
|
||||
net_play.camera = cam
|
||||
# §8.6 targetBoard.GetTargetVID() -> net_world:选中目标始终强显名字。
|
||||
if net_play.has_signal("target_changed") and net_world and net_world.has_method("set_target_vid"):
|
||||
@@ -236,6 +242,18 @@ func setup(m2client: Node, assets_root: String,
|
||||
if _assets != "":
|
||||
net_play.set_asset_root(_assets) # §3.5:加载 playersettingmodule.py 连击段表
|
||||
|
||||
# 主角必须先于后续的 proto/UI/远端实体重活建立。实网一次可收到近 200 个
|
||||
# GC_CHARACTER_ADD;若等整个 setup() 尾部才订阅,怪物模型同步加载会把本地角色
|
||||
# 卡在占位胶囊几十秒,并且装备控制器也会错过 entity_main_set。
|
||||
client.entity_main_set.connect(_on_main_set)
|
||||
_main_sync_ready = true
|
||||
if client.get_main_vid() != 0: # 信号可能在 GameScene 创建前已经派发
|
||||
if net_world and net_world.has_method("set_local_vid"):
|
||||
net_world.set_local_vid(client.get_main_vid())
|
||||
if net_play and net_play.has_method("_on_main_set"):
|
||||
net_play._on_main_set(client.get_main_vid())
|
||||
_on_main_set(client.get_main_vid())
|
||||
|
||||
await _yield()
|
||||
_audio = Audio.new()
|
||||
add_child(_audio)
|
||||
@@ -251,11 +269,15 @@ func setup(m2client: Node, assets_root: String,
|
||||
# UI 层 + 物品 proto + 背包窗(I 键开关)
|
||||
ui = UiManager.new()
|
||||
add_child(ui)
|
||||
quest_curtain = QuestCurtain.new()
|
||||
ui.add_child(quest_curtain)
|
||||
cursor_manager = CursorManager.new()
|
||||
add_child(cursor_manager)
|
||||
cursor_manager.setup(assets_root)
|
||||
mouse_controller = MouseController.new()
|
||||
add_child(mouse_controller)
|
||||
mouse_controller.setup(ui, cursor_manager, _audio)
|
||||
mouse_controller.world_drop_handler = _on_world_item_drop
|
||||
|
||||
# 聊天窗(Enter 聚焦输入)
|
||||
chat = ChatUI.new()
|
||||
@@ -290,17 +312,30 @@ func setup(m2client: Node, assets_root: String,
|
||||
break
|
||||
skill_fx = SkillFx.new()
|
||||
skill_fx.setup(fx, skill_table)
|
||||
if net_world:
|
||||
net_world.skill_table = skill_table
|
||||
net_world.fx = fx
|
||||
skills = SkillUI.new()
|
||||
add_child(skills)
|
||||
skills.setup(client, skill_table, ui)
|
||||
if skills.has_signal("skill_use_requested"):
|
||||
skills.skill_use_requested.connect(func(sid: int):
|
||||
if quickbar and quickbar.has_method("activate_skill_direct"):
|
||||
quickbar.activate_skill_direct(sid)
|
||||
)
|
||||
char_status_ui = CharStatusUI.new()
|
||||
add_child(char_status_ui)
|
||||
char_status_ui.setup(ui, client, assets_root)
|
||||
char_status_ui.setup(ui, client, assets_root, skill_table, mouse_controller)
|
||||
quickbar = Quickbar.new()
|
||||
add_child(quickbar)
|
||||
quickbar.item_mouse = mouse_controller
|
||||
quickbar.net_play = net_play # §3.8 修改 1:技能三层校验的运行期上下文
|
||||
quickbar.setup(client, skill_table, ui, func() -> Node: return player, assets_root)
|
||||
if hud:
|
||||
if "quickbar" in hud:
|
||||
hud.set("quickbar", quickbar)
|
||||
if hud.has_method("set_status_visible"):
|
||||
hud.set_status_visible(false)
|
||||
# §3.4 MODE_USE_SKILL:预约技能进射程后由 quickbar 执行实际施法。
|
||||
if net_play:
|
||||
net_play.use_skill_hook = func(slot: int) -> bool: return quickbar.activate_reserved(slot)
|
||||
@@ -322,6 +357,29 @@ func setup(m2client: Node, assets_root: String,
|
||||
if skill_table and skill_table.has_method("is_ranged") and skill_table.is_ranged(sid):
|
||||
_queue_shot(sid)
|
||||
)
|
||||
if quickbar.has_signal("character_requested"):
|
||||
quickbar.character_requested.connect(func():
|
||||
if char_status_ui: char_status_ui.toggle()
|
||||
)
|
||||
if quickbar.has_signal("inventory_requested"):
|
||||
quickbar.inventory_requested.connect(func():
|
||||
if inventory: inventory.toggle()
|
||||
)
|
||||
if quickbar.has_signal("messenger_requested"):
|
||||
quickbar.messenger_requested.connect(func():
|
||||
if friend_ui: friend_ui.toggle()
|
||||
)
|
||||
if quickbar.has_signal("safebox_requested"):
|
||||
quickbar.safebox_requested.connect(_on_safebox_button_clicked)
|
||||
if quickbar.has_signal("system_requested"):
|
||||
quickbar.system_requested.connect(func():
|
||||
if system_menu_ui: system_menu_ui.toggle()
|
||||
)
|
||||
if quickbar.has_signal("chat_requested"):
|
||||
quickbar.chat_requested.connect(func():
|
||||
if chat and chat.has_method("toggle_log"):
|
||||
chat.toggle_log()
|
||||
)
|
||||
|
||||
# 弓普攻起手(net_play._emit_swing):OnSetFlyTarget 已在那里发过 CG_FLY_TARGETING,
|
||||
# 这里只把 uSkill 压进 FLY 帧待发队列(普攻恒 0)。§3.6
|
||||
@@ -380,6 +438,13 @@ func setup(m2client: Node, assets_root: String,
|
||||
var refresh_affects := func(_a = null): if hud and hud.has_method("set_affects"): hud.set_affects(client.get_affects())
|
||||
client.affect_added.connect(refresh_affects)
|
||||
client.affect_removed.connect(refresh_affects)
|
||||
client.affect_added.connect(func(a: Dictionary):
|
||||
var t := int(a.get("type", 0))
|
||||
_set_player_affect(t, true)
|
||||
)
|
||||
client.affect_removed.connect(func(t: int):
|
||||
_set_player_affect(t, false)
|
||||
)
|
||||
await _yield()
|
||||
if ClassDB.class_exists("Metin2Proto") and assets_root != "":
|
||||
proto = ClassDB.instantiate("Metin2Proto")
|
||||
@@ -387,6 +452,8 @@ func setup(m2client: Node, assets_root: String,
|
||||
var ip := assets_root.path_join("locale/locale/en/item_proto")
|
||||
if FileAccess.file_exists(ip):
|
||||
proto.call("load_item_proto", ip)
|
||||
if quickbar and "proto" in quickbar:
|
||||
quickbar.proto = proto
|
||||
await _yield()
|
||||
if chat and chat.has_method("set_proto"):
|
||||
chat.set_proto(proto)
|
||||
@@ -408,8 +475,6 @@ func setup(m2client: Node, assets_root: String,
|
||||
net_world.title_name_resolver = func(g: int) -> String:
|
||||
var k := "PVP_LEVEL%d" % g
|
||||
return _tt_loc.t(k) if _tt_loc.has(k) else ""
|
||||
# 怪 / NPC 真模型:race -> mob_proto.name -> monster/npc 目录
|
||||
net_world.set_model_factory(_make_entity_model)
|
||||
if assets_root != "":
|
||||
item_list = ItemListDB.new()
|
||||
item_list.load_file(assets_root.path_join("locale/locale/common/item_list.txt"))
|
||||
@@ -424,8 +489,15 @@ func setup(m2client: Node, assets_root: String,
|
||||
equip_model = EquipModel.new()
|
||||
add_child(equip_model)
|
||||
equip_model.setup(client, item_list, func() -> Node: return player, assets_root)
|
||||
equip_model.effect_registry = fx
|
||||
equip_model.main_getter = func() -> int: return client.get_main_vid()
|
||||
equip_model.proto = proto # armor shape 走 item_proto values[3] + specular
|
||||
# 主角已在上面的优先路径建立时,装备相关网络信号早于 EquipModel 的订阅。
|
||||
# 用当前权威实体与装备快照补一次,保证首次进场立即挂盔甲、武器和发型。
|
||||
var current_main := int(client.get_main_vid())
|
||||
if current_main != 0:
|
||||
equip_model.set_race(int(client.get_entity(current_main).get("race", -1)))
|
||||
equip_model.refresh()
|
||||
# 地面掉落物
|
||||
ground_items = GroundItems.new()
|
||||
add_child(ground_items)
|
||||
@@ -438,12 +510,17 @@ func setup(m2client: Node, assets_root: String,
|
||||
if _poss_loc.has("POSSESSIVE_MORPHENE"):
|
||||
_possessive = _poss_loc.t("POSSESSIVE_MORPHENE")
|
||||
ground_items.setup(client, _mount, func() -> Node: return player, proto, item_list,
|
||||
func() -> Camera3D: return cam, _possessive)
|
||||
func() -> Camera3D: return cam, _possessive, func() -> Node: return world)
|
||||
pc.set_input_surfaces(cursor_manager, ui, ground_items,
|
||||
Callable(net_play, "cancel_fishing"), Callable(net_play, "on_ground_click"))
|
||||
Callable(net_play, "on_manual_move"), Callable(net_play, "on_ground_click"))
|
||||
view_equipment_ui = ViewEquipmentUI.new()
|
||||
add_child(view_equipment_ui)
|
||||
view_equipment_ui.setup(client, ui, proto, item_list)
|
||||
# 最后再升级怪/NPC/远端玩家真模型。实网的首次实体 burst 接近 200 个,
|
||||
# set_model_factory 会同步重建已有节点;放在主角装备和技能 UI 之前会让本地
|
||||
# 角色看似长期裸装,并使冒烟测试在控制器建立前误判完成。
|
||||
if net_world:
|
||||
net_world.set_model_factory(_make_entity_model)
|
||||
|
||||
# 选魔石窗(EventManager [SELECT_ITEM] -> interfacemodule.BINARY_OpenSelectItemWindow)
|
||||
select_item_ui = SelectItemUI.new()
|
||||
@@ -569,6 +646,13 @@ func setup(m2client: Node, assets_root: String,
|
||||
# §8.4「看他国玩家目标框」radio -> net_play(uitarget.TargetBoard.Open 前置过滤)。
|
||||
if net_play and net_play.has_method("set_view_other_empire_target") and game_option_ui.has_method("display_value"):
|
||||
net_play.set_view_other_empire_target(game_option_ui.display_value("target_board") == 1)
|
||||
# §8.4「聊天显示」radio -> chat.
|
||||
if chat and game_option_ui.has_method("display_value"):
|
||||
var vc: bool = (game_option_ui.display_value("view_chat") == 1)
|
||||
if chat.has_method("set_view_chat"):
|
||||
chat.set_view_chat(vc)
|
||||
elif "visible" in chat:
|
||||
chat.visible = vc
|
||||
system_menu_ui = SystemMenuUI.new()
|
||||
add_child(system_menu_ui)
|
||||
system_menu_ui.setup(ui, client, assets_root, system_option_ui, game_option_ui)
|
||||
@@ -582,24 +666,21 @@ func setup(m2client: Node, assets_root: String,
|
||||
if _ui_mobile and hud and hud.has_method("bind_controls"):
|
||||
_setup_mobile_ui()
|
||||
|
||||
client.entity_main_set.connect(_on_main_set)
|
||||
_main_sync_ready = true
|
||||
if client.get_main_vid() != 0: # 重连 / 已在局内
|
||||
# net_play._on_main_set 的信号早在它连上前就发过了 —— 手动补上关键那步:
|
||||
# 告诉 net_world 主角 vid 由本地 player 代表,别给它生成节点(否则 catch_up 会
|
||||
# 在玩家身上叠一个大蓝胶囊)。
|
||||
if net_world and net_world.has_method("set_local_vid"):
|
||||
net_world.set_local_vid(client.get_main_vid())
|
||||
if net_play and net_play.has_method("_on_main_set"):
|
||||
net_play._on_main_set(client.get_main_vid())
|
||||
_on_main_set(client.get_main_vid())
|
||||
|
||||
# setup() 是协程 —— 进来时进游戏的 spawn burst 早被 M2Client.pump_game 抽干、
|
||||
# net_world 还没连上信号。把当前所有实体(怪 / NPC / 别的玩家)补建一遍。
|
||||
if net_world and net_world.has_method("catch_up"):
|
||||
net_world.catch_up()
|
||||
|
||||
# 40250 IntroLoading 对齐:若主角真模型尚未构建完成,在场景就绪前同步构建
|
||||
if not _model_built and client and client.has_method("get_main_vid") and int(client.get_main_vid()) != 0:
|
||||
_sync_main_character()
|
||||
|
||||
set_process_unhandled_input(true)
|
||||
_setup_completed = true
|
||||
if pc:
|
||||
pc.active = true
|
||||
if pc.has_method("stop"):
|
||||
pc.stop()
|
||||
|
||||
func _setup_mobile_ui() -> void:
|
||||
# MobileUiRoot owns the presentation and touch layer; every feature node in
|
||||
@@ -649,6 +730,8 @@ func _drop_stale_pickables(drop_vid: int = -1) -> void:
|
||||
list.remove_at(i)
|
||||
elif n.is_queued_for_deletion():
|
||||
list.remove_at(i)
|
||||
elif bool(n.get_meta("dead", false)):
|
||||
list.remove_at(i)
|
||||
elif drop_vid >= 0 and int(n.get_meta("vid", -1)) == drop_vid:
|
||||
list.remove_at(i)
|
||||
|
||||
@@ -734,11 +817,13 @@ func _diag_tick() -> void:
|
||||
printerr("GAMESCENE_DIAG: ", JSON.stringify(d))
|
||||
# MT_DIAG_SHOT=<png>:最后一次 tick 存一张实机帧,用于人工核对渲染结果。
|
||||
var shot := OS.get_environment("MT_DIAG_SHOT")
|
||||
if not shot.is_empty() and _diag_done:
|
||||
var shot_ready := _model_built and is_instance_valid(equip_model) and is_instance_valid(quickbar)
|
||||
if not shot.is_empty() and (_diag_done or shot_ready):
|
||||
await RenderingServer.frame_post_draw
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
if img != null:
|
||||
img.save_png(shot)
|
||||
if img.save_png(shot) == OK:
|
||||
_diag_done = true
|
||||
|
||||
# 会徽上传图源:优先 res://ui/default_guild_mark.png,没有就现造一张 16×12 占位。
|
||||
func _guild_mark_upload_image() -> Image:
|
||||
@@ -846,7 +931,11 @@ func _on_local_motion_event(type: int, effect: String, sound: String, pos: Vecto
|
||||
|
||||
var _motion_effect_target: WeakRef
|
||||
|
||||
func _on_skill_cast_started(_skill: int, target_vid: int) -> void:
|
||||
func _on_skill_cast_started(skill: int, target_vid: int) -> void:
|
||||
if world and world.has_method("sample_height") and is_instance_valid(player):
|
||||
player.position.y = float(world.call("sample_height", player.position.x, player.position.z))
|
||||
if pc and pc.has_method("stop"):
|
||||
pc.stop()
|
||||
_motion_effect_target = null
|
||||
var target: Node3D = null
|
||||
if client and target_vid == int(client.get_main_vid()):
|
||||
@@ -855,6 +944,36 @@ func _on_skill_cast_started(_skill: int, target_vid: int) -> void:
|
||||
target = net_world.node_for(target_vid)
|
||||
if is_instance_valid(target):
|
||||
_motion_effect_target = weakref(target)
|
||||
if target != player and is_instance_valid(player):
|
||||
var to: Vector3 = target.global_position - player.global_position
|
||||
to.y = 0.0
|
||||
if to.length() > 0.01:
|
||||
player.rotation.y = atan2(to.x, to.z)
|
||||
var pv_node: Node = player if is_instance_valid(player) and player.has_method("play_skill_motion") else (net_play.player_view if net_play and "player_view" in net_play else null)
|
||||
var dur := 2.0
|
||||
var is_charge := false
|
||||
var cancel_enable := false
|
||||
if skill_table:
|
||||
if skill_table.has_method("is_charge_skill"):
|
||||
is_charge = skill_table.is_charge_skill(skill)
|
||||
if is_instance_valid(pv_node) and pv_node.has_method("play_skill_motion"):
|
||||
var mname: String = skill_table.motion_name_of(skill)
|
||||
if mname != "":
|
||||
var played: bool = pv_node.play_skill_motion(mname, _skill_master(skill))
|
||||
if played and "anim" in pv_node and pv_node.anim:
|
||||
if pv_node.anim.has_method("get_duration"):
|
||||
var d: float = float(pv_node.anim.get_duration())
|
||||
if d > 0.0:
|
||||
dur = d
|
||||
if pv_node.anim.has_method("get_loop_data"):
|
||||
var ld: Dictionary = pv_node.anim.get_loop_data()
|
||||
if bool(ld.get("cancel_enable", false)):
|
||||
cancel_enable = true
|
||||
if net_play:
|
||||
if net_play.has_method("start_skill_cast"):
|
||||
net_play.start_skill_cast(dur, is_charge, cancel_enable)
|
||||
else:
|
||||
net_play._using_skill = true
|
||||
|
||||
func _on_local_motion_event_detailed(event: Dictionary) -> void:
|
||||
# Keep sound/projectile dispatch exactly once, independent of visual routing.
|
||||
@@ -870,6 +989,10 @@ func _on_local_motion_event_detailed(event: Dictionary) -> void:
|
||||
else:
|
||||
_on_local_motion_event(int(event.type), String(event.effect), String(event.sound), event.pos)
|
||||
|
||||
func _set_player_affect(type: int, visible: bool) -> void:
|
||||
if player and player.has_method("set_affect"):
|
||||
player.set_affect(type, visible, fx)
|
||||
|
||||
# 一次弓挥击 / 弓技能起手 → 压一个待发 uSkill(对齐 AutoClear handler 的 Set())。
|
||||
# 满了丢最旧的:真客户端每个动作必到 FLY 帧,headless / 缺 `.msa` 时不至无界增长。§3.6
|
||||
func _queue_shot(skill: int) -> void:
|
||||
@@ -886,12 +1009,18 @@ func _on_quest_camera_event(kind: String, setting: Dictionary, blendtime: float)
|
||||
return
|
||||
match kind:
|
||||
"SET_CAMERA":
|
||||
if quest_curtain:
|
||||
quest_curtain.open()
|
||||
if cam.has_method("set_event_camera"):
|
||||
cam.set_event_camera(setting)
|
||||
"BLEND_CAMERA":
|
||||
if quest_curtain:
|
||||
quest_curtain.open(maxf(0.2, blendtime))
|
||||
if cam.has_method("blend_event_camera"):
|
||||
cam.blend_event_camera(setting, blendtime)
|
||||
"RESTORE_CAMERA":
|
||||
if quest_curtain:
|
||||
quest_curtain.close()
|
||||
if cam.has_method("set_default_camera"):
|
||||
cam.set_default_camera()
|
||||
|
||||
@@ -938,6 +1067,8 @@ func _clear_quest_presentation() -> void:
|
||||
if _quest_fade_rect and is_instance_valid(_quest_fade_rect):
|
||||
_quest_fade_rect.color.a = 0.0
|
||||
_quest_fade_rect.visible = false
|
||||
if quest_curtain:
|
||||
quest_curtain.close(0.2)
|
||||
if cam and cam.has_method("set_default_camera"):
|
||||
cam.set_default_camera()
|
||||
|
||||
@@ -1036,10 +1167,20 @@ func _place_player_at_net_pos(pos: Vector3) -> void:
|
||||
world.call("set_focus_position", wp.x, wp.z)
|
||||
if world and world.has_method("sample_height"):
|
||||
wp.y = float(world.call("sample_height", wp.x, wp.z))
|
||||
player.global_position = wp
|
||||
if net_play and "_last_sent_pos" in net_play:
|
||||
net_play._last_sent_pos = player.global_position
|
||||
if cam and cam.has_method("snap_to_target"):
|
||||
if player.is_inside_tree():
|
||||
player.global_position = wp
|
||||
else:
|
||||
player.position = wp
|
||||
if pc and pc.has_method("stop"):
|
||||
pc.stop()
|
||||
if net_play:
|
||||
if "_last_sent_pos" in net_play:
|
||||
net_play._last_sent_pos = player.global_position if player.is_inside_tree() else player.position
|
||||
if net_play.has_method("_clear_reserved"):
|
||||
net_play._clear_reserved()
|
||||
if net_play.has_method("_clear_auto_attack"):
|
||||
net_play._clear_auto_attack()
|
||||
if cam and cam.has_method("snap_to_target") and cam.is_inside_tree() and player.is_inside_tree():
|
||||
cam.snap_to_target()
|
||||
|
||||
# 背包道具右键:按当前打开的窗决定动作(商店卖 / 交易放 / 仓库存)。
|
||||
@@ -1188,7 +1329,7 @@ func _unhandled_input(e: InputEvent) -> void:
|
||||
_emote(n)
|
||||
elif _quick_page_mode and n < 4 and quickbar:
|
||||
quickbar.set_page(n)
|
||||
elif n < 4 and quickbar:
|
||||
elif n < 8 and quickbar:
|
||||
quickbar.activate(n)
|
||||
|
||||
func _set_names(visible: bool) -> void:
|
||||
@@ -1206,11 +1347,30 @@ func _on_display_option(key: String, value: int) -> void:
|
||||
net_world.set_empire_name_mode(value == 1)
|
||||
elif key == "target_board" and net_play and net_play.has_method("set_view_other_empire_target"):
|
||||
net_play.set_view_other_empire_target(value == 1)
|
||||
elif key == "view_chat" and chat:
|
||||
if chat.has_method("set_view_chat"):
|
||||
chat.set_view_chat(value == 1)
|
||||
elif "visible" in chat:
|
||||
chat.visible = (value == 1)
|
||||
|
||||
func _send_command(text: String) -> void:
|
||||
if client and client.has_method("say"):
|
||||
client.say(0, text)
|
||||
|
||||
func _on_safebox_button_clicked() -> void:
|
||||
if safebox_ui and safebox_ui.has_method("is_open") and safebox_ui.is_open():
|
||||
safebox_ui.close()
|
||||
elif safebox_ui and safebox_ui.has_method("open"):
|
||||
safebox_ui.open()
|
||||
# 对齐 40250 interfacemodule.py:OpenSafeboxWindow —— 打开仓库同时打开背包,
|
||||
# 让玩家能看到自己物品并右键存入仓库。
|
||||
if inventory and inventory.has_method("open"):
|
||||
inventory.open()
|
||||
elif mall_ui and mall_ui.has_method("is_open") and mall_ui.is_open():
|
||||
mall_ui.close()
|
||||
else:
|
||||
_send_command("/click_mall")
|
||||
|
||||
func _toggle_horse() -> void:
|
||||
if client == null:
|
||||
return
|
||||
@@ -1312,7 +1472,7 @@ func _make_entity_model(d: Dictionary) -> Node3D:
|
||||
return null
|
||||
var pv := RemotePlayerView.new()
|
||||
var parts: Array = d.get("parts", []) if d.get("parts", []) is Array else []
|
||||
if pv.build_remote(_assets, race, parts, item_list, proto, pump):
|
||||
if pv.build_remote(_assets, race, parts, item_list, proto, pump, fx):
|
||||
pv.set_audio(_audio)
|
||||
_remote_player_view_cache[race] = true
|
||||
return pv
|
||||
@@ -1337,11 +1497,14 @@ func set_player_model(node: Node3D) -> void:
|
||||
return
|
||||
var old := player
|
||||
node.position = old.position
|
||||
node.rotation = old.rotation
|
||||
node.name = "LocalPlayer"
|
||||
add_child(node)
|
||||
player = node
|
||||
cam.target = node
|
||||
pc.player = node
|
||||
if pc and pc.has_method("stop"):
|
||||
pc.stop()
|
||||
if net_world and net_world.has_method("set_local_node"):
|
||||
net_world.set_local_node(node)
|
||||
old.queue_free()
|
||||
@@ -1562,8 +1725,12 @@ func _on_main_set(vid: int) -> void:
|
||||
return
|
||||
if built:
|
||||
pv.set_audio(_audio)
|
||||
pv.set_fx(fx)
|
||||
set_player_model(pv)
|
||||
_model_built = true
|
||||
if client and client.has_method("get_affects"):
|
||||
for a in client.get_affects():
|
||||
pv.set_affect(int(a.get("type", 0)), true, fx)
|
||||
if net_play:
|
||||
net_play.player_view = pv
|
||||
if pv.anim and pv.anim.has_signal("motion_event_detailed"):
|
||||
@@ -1585,6 +1752,10 @@ func _on_main_set(vid: int) -> void:
|
||||
return
|
||||
_place_player_at_net_pos(net_pos)
|
||||
_main_placed_vid = vid
|
||||
if pc:
|
||||
pc.active = _setup_completed
|
||||
if pc.has_method("stop"):
|
||||
pc.stop()
|
||||
|
||||
func _create_main_view() -> Node3D:
|
||||
return PlayerView.new()
|
||||
@@ -1616,14 +1787,29 @@ func lifecycle_audio() -> Node:
|
||||
func _make_placeholder_player() -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "LocalPlayer"
|
||||
var mesh := MeshInstance3D.new()
|
||||
var cap := CapsuleMesh.new()
|
||||
cap.radius = 0.35
|
||||
cap.height = 1.8
|
||||
mesh.mesh = cap
|
||||
mesh.position.y = 0.9
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.35, 0.6, 1.0)
|
||||
mesh.material_override = mat
|
||||
root.add_child(mesh)
|
||||
return root
|
||||
|
||||
func _on_world_item_drop(payload: Dictionary, screen_pos: Vector2) -> bool:
|
||||
if pc == null or pc.camera == null or client == null:
|
||||
return false
|
||||
var cam: Camera3D = pc.camera
|
||||
var from := cam.project_ray_origin(screen_pos)
|
||||
var dir := cam.project_ray_normal(screen_pos)
|
||||
var best: Node3D = null
|
||||
var best_t := 1e20
|
||||
for n in pc.pickables:
|
||||
if not is_instance_valid(n) or bool(n.get_meta("dead", false)):
|
||||
continue
|
||||
var t := float(pc.call("_ray_pick_t", from, dir, n)) if pc.has_method("_ray_pick_t") else -1.0
|
||||
if t >= 0.0 and t < best_t:
|
||||
best_t = t
|
||||
best = n
|
||||
if best != null:
|
||||
var target_vid := int(best.get_meta("vid", 0))
|
||||
if target_vid > 0:
|
||||
var win := int(payload.get("window", 1))
|
||||
var cell := int(payload.get("cell", 0))
|
||||
var count := int(payload.get("count", 1))
|
||||
if client.has_method("give_item"):
|
||||
return bool(client.give_item(target_vid, win, cell, count))
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user