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:
+126
-12
@@ -153,8 +153,65 @@ bool add_attribute_collision(const fmt::AttributeCollision &collision,
|
||||
return true;
|
||||
}
|
||||
|
||||
inline float cross_product_2d(float x1, float y1, float x2, float y2) {
|
||||
return x1 * y2 - y1 * x2;
|
||||
}
|
||||
|
||||
inline bool is_in_triangle_2d(float ax, float ay, float bx, float by, float cx, float cy, float tx, float ty) {
|
||||
float c1 = cross_product_2d(bx - ax, by - ay, tx - ax, ty - ay);
|
||||
float c2 = cross_product_2d(cx - bx, cy - by, tx - bx, ty - by);
|
||||
float c3 = cross_product_2d(ax - cx, ay - cy, tx - cx, ty - cy);
|
||||
|
||||
if (c1 * c2 > 0.0f && c1 * c3 > 0.0f)
|
||||
return true;
|
||||
|
||||
if (c1 * c2 * c3 == 0.0f) {
|
||||
if (tx < ax && tx < bx && tx < cx) return false;
|
||||
if (tx > ax && tx > bx && tx > cx) return false;
|
||||
if (ty < ay && ty < by && ty < cy) return false;
|
||||
if (ty > ay && ty > by && ty > cy) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void extract_height_triangles(const fmt::AttributeData &data, const Transform3D &place,
|
||||
std::vector<Metin2World::HeightTriangle> &out_heights) {
|
||||
for (const auto &height : data.heights) {
|
||||
if (height.vertices.size() < 3)
|
||||
continue;
|
||||
for (size_t i = 0; i + 2 < height.vertices.size(); i += 3) {
|
||||
Vector3 v0 = place.xform(attribute_position(height.vertices[i]));
|
||||
Vector3 v1 = place.xform(attribute_position(height.vertices[i + 1]));
|
||||
Vector3 v2 = place.xform(attribute_position(height.vertices[i + 2]));
|
||||
Vector3 line1 = v1 - v0;
|
||||
Vector3 line2 = v2 - v0;
|
||||
Vector3 normal = line1.cross(line2);
|
||||
float len = normal.length();
|
||||
if (len < 1e-6f)
|
||||
continue;
|
||||
normal /= len;
|
||||
if (std::abs(normal.y) < 0.001f)
|
||||
continue; // vertical or near-vertical surface, not a walkable floor/deck
|
||||
Metin2World::HeightTriangle tri;
|
||||
tri.v0 = v0;
|
||||
tri.v1 = v1;
|
||||
tri.v2 = v2;
|
||||
tri.min_x = std::min({v0.x, v1.x, v2.x});
|
||||
tri.max_x = std::max({v0.x, v1.x, v2.x});
|
||||
tri.min_z = std::min({v0.z, v1.z, v2.z});
|
||||
tri.max_z = std::max({v0.z, v1.z, v2.z});
|
||||
tri.normal = normal;
|
||||
tri.d = normal.dot(v0);
|
||||
out_heights.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int add_attribute_data(const std::string &path, const Transform3D &place,
|
||||
Node3D *root, MeshInstance3D *mesh) {
|
||||
Node3D *root, MeshInstance3D *mesh,
|
||||
std::vector<Metin2World::HeightTriangle> *out_heights = nullptr) {
|
||||
fmt::AttributeData data;
|
||||
std::string error;
|
||||
if (!fmt::parse_attribute_data_file(path, data, &error)) {
|
||||
@@ -162,6 +219,9 @@ int add_attribute_data(const std::string &path, const Transform3D &place,
|
||||
" (" + error.c_str() + ")");
|
||||
return 0;
|
||||
}
|
||||
if (out_heights) {
|
||||
extract_height_triangles(data, place, *out_heights);
|
||||
}
|
||||
StaticBody3D *body = memnew(StaticBody3D);
|
||||
body->set_name(String("AttributeBody_") + String(path.c_str()).get_file().get_basename());
|
||||
body->set_collision_layer(2);
|
||||
@@ -196,6 +256,15 @@ int add_attribute_data(const std::string &path, const Transform3D &place,
|
||||
return shapes;
|
||||
}
|
||||
|
||||
void load_attribute_heights(const std::string &path, const Transform3D &place,
|
||||
std::vector<Metin2World::HeightTriangle> &out_heights) {
|
||||
fmt::AttributeData data;
|
||||
std::string error;
|
||||
if (fmt::parse_attribute_data_file(path, data, &error)) {
|
||||
extract_height_triangles(data, place, out_heights);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Metin2World::Metin2World() {}
|
||||
@@ -468,7 +537,7 @@ bool Metin2World::build_chunk(int tx, int ty) {
|
||||
ck.am = am;
|
||||
ck.root = croot;
|
||||
if (objects_enabled && registry_ok && resolver)
|
||||
place_chunk_objects(tx, ty, croot, ck.objects, ck.trees);
|
||||
place_chunk_objects(tx, ty, croot, ck.objects, ck.trees, ck.height_triangles);
|
||||
if (registry_ok)
|
||||
place_chunk_ambience(tx, ty);
|
||||
objects_placed += ck.objects;
|
||||
@@ -477,7 +546,8 @@ bool Metin2World::build_chunk(int tx, int ty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj, int &n_tree) {
|
||||
void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj, int &n_tree,
|
||||
std::vector<HeightTriangle> &out_heights) {
|
||||
n_obj = 0;
|
||||
n_tree = 0;
|
||||
const std::string mdir = std::string(map_dir().utf8().get_data());
|
||||
@@ -576,13 +646,16 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
|
||||
// 盒尺寸/中心直接算到「米 / Y-up」,body 只带 place(旋转+平移,无 conv),
|
||||
// 避免把碰撞形状放在 0.01 缩放节点下(Godot 缩放 shape 不稳)。
|
||||
bool mdatr_built = false;
|
||||
if (collision_enabled) {
|
||||
String md = String(rp.c_str()).get_basename() + ".mdatr";
|
||||
if (FileAccess::file_exists(md)) {
|
||||
const int built = add_attribute_data(std::string(md.utf8().get_data()), place, root, mi);
|
||||
String md = String(rp.c_str()).get_basename() + ".mdatr";
|
||||
if (FileAccess::file_exists(md)) {
|
||||
const std::string md_str(md.utf8().get_data());
|
||||
if (collision_enabled) {
|
||||
const int built = add_attribute_data(md_str, place, root, mi, &out_heights);
|
||||
mdatr_built = built > 0;
|
||||
if (mdatr_built)
|
||||
++objects_mdatr_built;
|
||||
} else {
|
||||
load_attribute_heights(md_str, place, out_heights);
|
||||
}
|
||||
}
|
||||
// A malformed/missing attribute resource still gets a conservative
|
||||
@@ -880,11 +953,52 @@ double Metin2World::sample_height(double gx_m, double gz_m) const {
|
||||
const int tx = int(mx_cm / fmt::m2coord::CHUNK_CM);
|
||||
const int ty = int(my_abs_cm / fmt::m2coord::CHUNK_CM);
|
||||
const Chunk *c = chunk_at(tx, ty);
|
||||
if (!c || !c->hm)
|
||||
return 0.0;
|
||||
const double lx = mx_cm - double(tx) * fmt::m2coord::CHUNK_CM;
|
||||
const double ly = my_abs_cm - double(ty) * fmt::m2coord::CHUNK_CM;
|
||||
return fmt::terrain_height_at(*c->hm, lx, ly, setting.height_scale) * fmt::m2coord::CM_TO_M;
|
||||
double terrain_h = 0.0;
|
||||
if (c && c->hm) {
|
||||
const double lx = mx_cm - double(tx) * fmt::m2coord::CHUNK_CM;
|
||||
const double ly = my_abs_cm - double(ty) * fmt::m2coord::CHUNK_CM;
|
||||
terrain_h = fmt::terrain_height_at(*c->hm, lx, ly, setting.height_scale) * fmt::m2coord::CM_TO_M;
|
||||
}
|
||||
|
||||
// 40250 CMapOutdoor::GetHeight(fx, fy):
|
||||
// Check placed objects' height data (bridges, stairs, platforms from .mdatr).
|
||||
// If object height > terrain height, return fMAX(fObjectHeight, fTerrainHeight).
|
||||
float obj_h = -25000.0f;
|
||||
bool obj_found = false;
|
||||
|
||||
const float fgx = float(gx_m);
|
||||
const float fgz = float(gz_m);
|
||||
|
||||
for (int dtx = -1; dtx <= 1; ++dtx) {
|
||||
int ntx = tx + dtx;
|
||||
if (ntx < 0 || ntx >= setting.map_size_x)
|
||||
continue;
|
||||
for (int dty = -1; dty <= 1; ++dty) {
|
||||
int nty = ty + dty;
|
||||
if (nty < 0 || nty >= setting.map_size_y)
|
||||
continue;
|
||||
const Chunk *nc = chunk_at(ntx, nty);
|
||||
if (!nc || nc->height_triangles.empty())
|
||||
continue;
|
||||
for (const auto &tri : nc->height_triangles) {
|
||||
if (fgx < tri.min_x || fgx > tri.max_x ||
|
||||
fgz < tri.min_z || fgz > tri.max_z)
|
||||
continue;
|
||||
if (is_in_triangle_2d(tri.v0.x, tri.v0.z, tri.v1.x, tri.v1.z, tri.v2.x, tri.v2.z, fgx, fgz)) {
|
||||
float h = (tri.d - tri.normal.x * fgx - tri.normal.z * fgz) / tri.normal.y;
|
||||
if (!obj_found || h > obj_h) {
|
||||
obj_h = h;
|
||||
obj_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj_found && (c == nullptr || !c->hm || obj_h > terrain_h))
|
||||
return double(obj_h);
|
||||
|
||||
return terrain_h;
|
||||
}
|
||||
|
||||
Ref<godot::Image> Metin2World::load_dds(const String &path) const {
|
||||
|
||||
Reference in New Issue
Block a user