Implement 40250 classic client port
This commit is contained in:
+95
-1
@@ -22,6 +22,7 @@ signal main_entity_ready(node: Node3D, vid: int)
|
||||
signal chat_line(type: int, vid: int, text: String)
|
||||
signal fly_targeting(shooter_vid: int, target_vid: int, target_cm: Vector2, append: bool)
|
||||
signal fishing_event(vid: int, subheader: int, dir: int)
|
||||
signal motion_event(vid: int, victim_vid: int, motion: int)
|
||||
signal vitals(vid: int, hp: int, max_hp: int, dead: bool)
|
||||
signal damage_number(vid: int, amount: int, crit: bool, dodge: bool)
|
||||
|
||||
@@ -47,9 +48,12 @@ var snap_dist := 6.0 # 超过这么多米直接瞬移(初次
|
||||
var _by_vid := {} # vid:int -> Node3D
|
||||
var _main_vid := 0
|
||||
var _local_vid := 0 # 由 net_play 设:这个 vid 由本地 player 代表,不生成节点
|
||||
var _local_node: Node3D # 本地玩家的真模型(不挂在 _mount 下)
|
||||
var _model_factory: Callable # func(Dictionary) -> Node3D
|
||||
var name_resolver: Callable = Callable() # func(entity_dict) -> String(怪名走 mob_proto)
|
||||
var _pvp_relations := {} # unordered pair key -> {src_vid,dst_vid,mode}
|
||||
var _duel_opponents := {} # server-provided duel opponent VID set
|
||||
var _duel_cannot_attack := false
|
||||
|
||||
func setup(m2client: Node, mount: Node3D) -> void:
|
||||
client = m2client
|
||||
@@ -68,6 +72,8 @@ func setup(m2client: Node, mount: Node3D) -> void:
|
||||
client.vitals_changed.connect(_on_vitals)
|
||||
client.entity_dead.connect(_on_dead)
|
||||
client.damage.connect(_on_damage)
|
||||
if client.has_signal("motion"):
|
||||
client.motion.connect(_on_motion)
|
||||
if client.has_signal("dig_motion"):
|
||||
client.dig_motion.connect(_on_dig_motion)
|
||||
if client.has_signal("fly_cue"):
|
||||
@@ -78,6 +84,8 @@ func setup(m2client: Node, mount: Node3D) -> void:
|
||||
client.fishing_event.connect(_on_fishing_event)
|
||||
if client.has_signal("pvp_changed"):
|
||||
client.pvp_changed.connect(_on_pvp_changed)
|
||||
if client.has_signal("duel_changed"):
|
||||
client.duel_changed.connect(_on_duel_changed)
|
||||
|
||||
# 已在局内(重连 / setup 是协程,进来时 spawn burst 已被 pump 抽干)——
|
||||
# 把当前所有实体补建一遍,别漏掉进游戏那一批怪 / NPC / 玩家。
|
||||
@@ -92,6 +100,8 @@ func catch_up() -> void:
|
||||
for relation in client.get_pvp_relations():
|
||||
_on_pvp_changed(int(relation.get("src_vid", 0)), int(relation.get("dst_vid", 0)),
|
||||
int(relation.get("mode", 0)))
|
||||
if client.has_method("get_duel"):
|
||||
_on_duel_changed(client.get_duel())
|
||||
|
||||
func set_model_factory(f: Callable) -> void:
|
||||
_model_factory = f
|
||||
@@ -104,6 +114,9 @@ func set_local_vid(vid: int) -> void:
|
||||
_by_vid.erase(vid)
|
||||
n.queue_free()
|
||||
|
||||
func set_local_node(node: Node3D) -> void:
|
||||
_local_node = node
|
||||
|
||||
func node_for(vid: int) -> Node3D:
|
||||
return _by_vid.get(vid, null)
|
||||
|
||||
@@ -129,6 +142,7 @@ func _on_spawn(d: Dictionary) -> void:
|
||||
node.set_meta("vid", vid)
|
||||
node.set_meta("func", int(d.get("func", FUNC_WAIT)))
|
||||
_by_vid[vid] = node
|
||||
_refresh_shop_sign(node, str(d.get("shop_sign", "")))
|
||||
_refresh_pvp_tag(vid)
|
||||
entity_added.emit(node, vid)
|
||||
if vid == _main_vid:
|
||||
@@ -145,6 +159,26 @@ func _on_info(vid: int, d: Dictionary) -> void:
|
||||
(n.get_node("Label3D") as Label3D).text = nm
|
||||
elif n.has_method("set_display_name"):
|
||||
n.set_display_name(nm)
|
||||
_refresh_shop_sign(n, str(d.get("shop_sign", "")))
|
||||
|
||||
func _refresh_shop_sign(root: Node3D, sign: String) -> void:
|
||||
var tag := root.get_node_or_null("ShopSign") as Label3D
|
||||
var text := sign.strip_edges()
|
||||
if text == "":
|
||||
if tag:
|
||||
tag.queue_free()
|
||||
return
|
||||
if tag == null:
|
||||
tag = Label3D.new()
|
||||
tag.name = "ShopSign"
|
||||
tag.position.y = 3.0
|
||||
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
tag.no_depth_test = true
|
||||
tag.pixel_size = 0.0055
|
||||
tag.modulate = Color(1.0, 0.82, 0.25)
|
||||
tag.outline_size = 8
|
||||
root.add_child(tag)
|
||||
tag.text = text
|
||||
|
||||
func _display_name(d: Dictionary) -> String:
|
||||
if name_resolver.is_valid():
|
||||
@@ -154,6 +188,7 @@ func _display_name(d: Dictionary) -> String:
|
||||
return str(d.get("name", ""))
|
||||
|
||||
func _on_despawn(vid: int) -> void:
|
||||
_duel_opponents.erase(vid)
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
if n:
|
||||
_by_vid.erase(vid)
|
||||
@@ -169,6 +204,50 @@ func _on_main_set(vid: int) -> void:
|
||||
func _on_moved(_vid: int) -> void:
|
||||
pass # 插值在 _process 里统一做
|
||||
|
||||
# GC_MOTION and ServerCommand emotion commands share CRaceMotionData IDs in
|
||||
# the 40250 client. Keep the raw event visible to gameplay and ask real PC
|
||||
# views to play the matching one-shot action; placeholders still retain the
|
||||
# authoritative motion metadata and state tint.
|
||||
func _on_motion(vid: int, victim_vid: int, motion: int) -> void:
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
if n == null and vid == _local_vid:
|
||||
n = _local_node
|
||||
motion_event.emit(vid, victim_vid, motion)
|
||||
if n == null:
|
||||
return
|
||||
n.set_meta("last_motion", motion)
|
||||
n.set_meta("motion_victim_vid", victim_vid)
|
||||
var target: Node3D = _by_vid.get(victim_vid, null)
|
||||
if target and target != n:
|
||||
var flat := target.global_position
|
||||
flat.y = n.global_position.y
|
||||
if n.global_position.distance_squared_to(flat) > 0.001:
|
||||
n.look_at(flat, Vector3.UP)
|
||||
var target_race := -1
|
||||
if client:
|
||||
var target_entity: Dictionary = client.get_entity(victim_vid)
|
||||
if not target_entity.is_empty():
|
||||
target_race = int(target_entity.get("race", -1))
|
||||
var applied := false
|
||||
if n.has_method("set_motion_id"):
|
||||
applied = bool(n.call("set_motion_id", motion, target_race))
|
||||
if not applied:
|
||||
var state := "attack"
|
||||
if motion >= 305:
|
||||
state = "emotion"
|
||||
elif motion >= 1 and motion <= 3:
|
||||
state = "wait" if motion == 1 else ("walk" if motion == 2 else "run")
|
||||
elif motion == 5 or motion == 6 or motion == 8 or motion == 9:
|
||||
state = "damage"
|
||||
elif motion == 11 or motion == 12:
|
||||
state = "dead"
|
||||
if n.has_method("set_anim_state"):
|
||||
n.call("set_anim_state", state)
|
||||
else:
|
||||
n.set_meta("motion_state", state)
|
||||
if n.has_node("Label3D"):
|
||||
(n.get_node("Label3D") as Label3D).modulate = _state_tint(state)
|
||||
|
||||
# GC_PVP: 显示当前与任一角色的挑战 / 战斗 / 复仇关系。原客户端同时会影响
|
||||
# TargetBoard 和名字颜色;这里先提供始终可见的世界标签,避免关系状态无表现。
|
||||
func _on_pvp_changed(src_vid: int, dst_vid: int, mode: int) -> void:
|
||||
@@ -197,7 +276,8 @@ func _refresh_pvp_tag(vid: int) -> void:
|
||||
return
|
||||
var mode := _pvp_mode_for(vid)
|
||||
var tag := n.get_node_or_null("PvpTag") as Label3D
|
||||
if mode == 0:
|
||||
var in_duel := _duel_opponents.has(vid)
|
||||
if mode == 0 and not in_duel:
|
||||
if tag:
|
||||
tag.queue_free()
|
||||
return
|
||||
@@ -209,6 +289,10 @@ func _refresh_pvp_tag(vid: int) -> void:
|
||||
tag.no_depth_test = true
|
||||
tag.pixel_size = 0.0045
|
||||
n.add_child(tag)
|
||||
if in_duel:
|
||||
tag.text = "决斗"
|
||||
tag.modulate = Color(0.45, 0.85, 1.0)
|
||||
return
|
||||
match mode:
|
||||
1:
|
||||
tag.text = "挑战"
|
||||
@@ -223,6 +307,16 @@ func _refresh_pvp_tag(vid: int) -> void:
|
||||
tag.text = "PVP"
|
||||
tag.modulate = Color.WHITE
|
||||
|
||||
func _on_duel_changed(duel: Dictionary) -> void:
|
||||
_duel_opponents.clear()
|
||||
_duel_cannot_attack = bool(duel.get("cannot_attack", false))
|
||||
for vid in duel.get("opponents", []):
|
||||
var opponent := int(vid)
|
||||
if opponent != 0:
|
||||
_duel_opponents[opponent] = true
|
||||
for vid in _by_vid.keys():
|
||||
_refresh_pvp_tag(int(vid))
|
||||
|
||||
# 头顶聊天气泡(~4s 淡出)。本地玩家 vid 走 main_bubble 信号让上层处理。
|
||||
signal main_bubble(text: String)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user