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:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+82 -19
View File
@@ -36,6 +36,16 @@ const INDEX_FLY_TYPE_NORMAL := 0
const INDEX_FLY_TYPE_FIRE_CRACKER := 1
const INDEX_FLY_TYPE_AUTO_FIRE := 2
# 40250 playersettingmodule.py / char.h 索引飞行物类型 (effect.FLY_*)
const FLY_EXP := 0
const FLY_HP_SMALL := 1
const FLY_HP_MEDIUM := 2
const FLY_HP_BIG := 3
const FLY_SP_SMALL := 4
const FLY_SP_MEDIUM := 5
const FLY_SP_BIG := 6
const FLY_FIREWORK1 := 7
# CFlyingData —— .fly 脚本字段。POC 无 .fly 加载器,默认值逐项对齐 CFlyingData::__Initialize()
# (长度单位:原版是 pixel==cm,这里换算成米后传进来,故 init_vel / range / bomb_range 都按米)。
class FlyData extends RefCounted:
@@ -80,7 +90,10 @@ class FlyInstance extends RefCounted:
func target_position() -> Vector3:
if _is_object and is_instance_valid(_target_obj):
_target_pos = _target_obj.global_position
if _target_obj.is_inside_tree():
_target_pos = _target_obj.global_position
else:
_target_pos = _target_obj.position
var tp := _target_pos
if data and data.maintain_parallel:
tp.y += 0.5
@@ -100,7 +113,7 @@ class FlyInstance extends RefCounted:
if target is Node3D:
_is_object = true
_target_obj = target
_target_pos = (target as Node3D).global_position
_target_pos = (target as Node3D).global_position if (target as Node3D).is_inside_tree() else (target as Node3D).position
elif target is Vector3:
_is_object = false
_target_pos = target
@@ -223,6 +236,7 @@ var _id_counter := 1
signal shoot_damage(target_vid: int)
signal exploded(world_pos: Vector3, cause: String)
signal exp_absorbed(world_pos: Vector3)
func setup(mount: Node3D, w: Object = null) -> void:
parent = mount
@@ -244,16 +258,41 @@ func spawn(start_world: Vector3, target, can_attack: bool, data: FlyData = null,
if parent != null:
var m := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.08
sm.height = 0.16
m.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.9, 0.4)
mat.emission_enabled = true
mat.emission = Color(0.9, 0.7, 0.2)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
if skill_index == FLY_EXP:
# 40250 官方经验聚能光球: ga_piece_yellow_small2.msf
sm.radius = 0.12
sm.height = 0.24
mat.albedo_color = Color(1.0, 0.9, 0.2, 0.95)
mat.emission = Color(1.0, 0.8, 0.1) * 3.0
elif skill_index in [FLY_HP_SMALL, FLY_HP_MEDIUM, FLY_HP_BIG]:
# 40250 官方生命吸取光球: ga_piece_red_small.msf
sm.radius = 0.12
sm.height = 0.24
mat.albedo_color = Color(1.0, 0.25, 0.25, 0.95)
mat.emission = Color(1.0, 0.1, 0.1) * 3.0
elif skill_index in [FLY_SP_SMALL, FLY_SP_MEDIUM, FLY_SP_BIG]:
# 40250 官方法力吸取光球: ga_piece_blue_small.msf
sm.radius = 0.12
sm.height = 0.24
mat.albedo_color = Color(0.25, 0.6, 1.0, 0.95)
mat.emission = Color(0.1, 0.5, 1.0) * 3.0
else:
sm.radius = 0.08
sm.height = 0.16
mat.albedo_color = Color(1.0, 0.9, 0.4)
mat.emission = Color(0.9, 0.7, 0.2)
m.mesh = sm
m.material_override = mat
parent.add_child(m)
m.global_position = inst.pos
if m.is_inside_tree():
m.global_position = inst.pos
else:
m.position = inst.pos
_visuals[inst] = m
return inst
@@ -268,7 +307,10 @@ func step(dt: float) -> void:
var keep := inst.update(dt)
var vis: MeshInstance3D = _visuals.get(inst, null)
if is_instance_valid(vis):
vis.global_position = inst.pos
if vis.is_inside_tree():
vis.global_position = inst.pos
else:
vis.position = inst.pos
if not keep:
if is_instance_valid(vis):
vis.queue_free()
@@ -287,28 +329,49 @@ func _on_instance_event(inst: FlyInstance, ev: String, wp: Vector3, vid: int) ->
shoot_damage.emit(vid)
"out_of_range", "at_target", "at_position", "at_background", "at_another":
exploded.emit(wp, ev)
if ev == "at_target" and inst.skill_index == FLY_EXP:
exp_absorbed.emit(wp)
if parent != null and ev != "out_of_range":
_spawn_flash(wp)
_spawn_flash(wp, inst.skill_index)
"bomb":
if parent != null:
_spawn_flash(wp)
_spawn_flash(wp, inst.skill_index)
func _spawn_flash(wp: Vector3) -> void:
func _spawn_flash(wp: Vector3, skill_index: int = -1) -> void:
var f := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.05
sm.height = 0.1
sm.radius = 0.06
sm.height = 0.12
f.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.75, 0.3, 0.9)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.6, 0.2)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
var final_scale := Vector3.ONE * 6.0
if skill_index == FLY_EXP:
mat.albedo_color = Color(1.0, 0.9, 0.2, 0.95)
mat.emission = Color(1.0, 0.85, 0.2) * 2.5
final_scale = Vector3.ONE * 8.0
elif skill_index in [FLY_HP_SMALL, FLY_HP_MEDIUM, FLY_HP_BIG]:
mat.albedo_color = Color(1.0, 0.25, 0.25, 0.95)
mat.emission = Color(1.0, 0.1, 0.1) * 2.5
final_scale = Vector3.ONE * 8.0
elif skill_index in [FLY_SP_SMALL, FLY_SP_MEDIUM, FLY_SP_BIG]:
mat.albedo_color = Color(0.25, 0.6, 1.0, 0.95)
mat.emission = Color(0.1, 0.5, 1.0) * 2.5
final_scale = Vector3.ONE * 8.0
else:
mat.albedo_color = Color(1.0, 0.75, 0.3, 0.9)
mat.emission = Color(1.0, 0.6, 0.2)
f.material_override = mat
parent.add_child(f)
f.global_position = wp
if f.is_inside_tree():
f.global_position = wp
else:
f.position = wp
var tw := create_tween()
tw.set_parallel(true)
tw.tween_property(f, "scale", Vector3.ONE * 6.0, 0.25)
tw.tween_property(mat, "albedo_color:a", 0.0, 0.25)
tw.tween_property(f, "scale", final_scale, 0.28)
tw.tween_property(mat, "albedo_color:a", 0.0, 0.28)
tw.chain().tween_callback(f.queue_free)