Implement 40250 classic client port

This commit is contained in:
shenlei
2026-09-01 18:49:28 +09:00
parent 2277b1dd60
commit 8f61990a83
481 changed files with 169826 additions and 293 deletions
+95 -1
View File
@@ -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)