diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 66f8deb..bad21f8 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -227,9 +227,26 @@ void // to terrain). { extern Entity *gBTTerrainEntity; + // The boresight ground/structure pick needs a non-null world Entity to + // cite in mech+0x388 (the geometry rides in the pick POINT; the entity is + // only the non-mech designation -- the decomp's HudSimulation handles a + // target with no damage-zone table, part_013.c:5620). Previously this + // required a TERRAIN-derived entity, but maps whose world geometry is + // buildings/cultural/props (class-42, NOT Terrain-derived) captured + // nothing -> gBTTerrainEntity stayed 0 -> the ground pick was skipped + // (mech4.cpp:4372) -> only mechs were targetable/firable (task #50 + // regression: LAST.EGG rewrite lost its Terrain entity). This `default` + // case is world-geometry ONLY (mechs route elsewhere), so capture the + // first entity reaching it -- preferring a real Terrain, else any world + // entity (building/prop/landmark) as the non-mech pick sentinel. if (gBTTerrainEntity == 0 - && entity->IsDerivedFrom(*Terrain::GetClassDerivations())) - gBTTerrainEntity = entity; + || !gBTTerrainEntity->IsDerivedFrom(*Terrain::GetClassDerivations())) + { + if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) + gBTTerrainEntity = entity; // prefer a true Terrain entity + else if (gBTTerrainEntity == 0) + gBTTerrainEntity = entity; // else the first world entity + } } DPLRenderer::MakeEntityRenderables( entity, model_resource, view_type); diff --git a/game/reconstructed/btvisgnd.cpp b/game/reconstructed/btvisgnd.cpp index 55ec007..5ec4ded 100644 --- a/game/reconstructed/btvisgnd.cpp +++ b/game/reconstructed/btvisgnd.cpp @@ -53,6 +53,7 @@ struct VgMesh { std::vector tris; float minx, maxx, minz, maxz; // model-local XZ bound + float miny, maxy; // model-local Y bound (dome vs flat-ground test) }; struct VgInst @@ -89,8 +90,8 @@ int LoadMeshFor(const char *resource_name) if (LoadBgfFile(file, data) && data.ok && !data.indices.empty()) { VgMesh m; - m.minx = m.minz = 1e9f; - m.maxx = m.maxz = -1e9f; + m.minx = m.minz = m.miny = 1e9f; + m.maxx = m.maxz = m.maxy = -1e9f; // The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then // (a,c,b) -- 6 indices per source triangle. Take the forward one. for (size_t i = 0; i + 5 < data.indices.size(); i += 6) @@ -100,17 +101,33 @@ int LoadMeshFor(const char *resource_name) const BgfVtx &C = data.verts[data.indices[i + 2]]; VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z }; m.tris.push_back(t); - float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }; + float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }, ys[3] = { A.y, B.y, C.y }; for (int k = 0; k < 3; ++k) { if (xs[k] < m.minx) m.minx = xs[k]; if (xs[k] > m.maxx) m.maxx = xs[k]; if (zs[k] < m.minz) m.minz = zs[k]; if (zs[k] > m.maxz) m.maxz = zs[k]; + if (ys[k] < m.miny) m.miny = ys[k]; + if (ys[k] > m.maxy) m.maxy = ys[k]; } } if (!m.tris.empty()) { + if (getenv("BT_GROUND_LOG")) + { + float miny = 1e9f, maxy = -1e9f; + for (size_t j = 0; j < m.tris.size(); ++j) + { + const VgTri &t = m.tris[j]; + const float ys[3] = { t.ay, t.by, t.cy }; + for (int k = 0; k < 3; ++k) + { if (ys[k] < miny) miny = ys[k]; if (ys[k] > maxy) maxy = ys[k]; } + } + DEBUG_STREAM << "[visgnd] mesh '" << key << "' tris=" << m.tris.size() + << " x[" << m.minx << "," << m.maxx << "] z[" << m.minz << "," << m.maxz + << "] y[" << miny << "," << maxy << "]\n" << std::flush; + } gMeshes.push_back(m); idx = (int)gMeshes.size() - 1; } @@ -156,11 +173,31 @@ bool BuildTables() ResourceDescription *rd = rf->FindResourceDescription(rid); if (rd == 0) continue; - if (SkippedName(rd->resourceName)) { ++skipped; continue; } int mi = LoadMeshFor(rd->resourceName); if (mi < 0) { ++skipped; continue; } + // GEOMETRY-AWARE skip: the name filter (sky/backdrop) is right for a high + // vertical DOME, but arena1's ground is a 10000x10000 FLAT plane at y=0 + // misnamed "sky" -- name-skipping it removed the only groundable geometry + // (task #50: buildings/ground stopped moving the range axis). Keep a + // name-skipped mesh IF it is a wide, near-flat, near-ground plane (the + // arena floor); still skip true domes / tall vertical backdrops. + if (SkippedName(rd->resourceName)) + { + const VgMesh &gm = gMeshes[mi]; + const float yext = gm.maxy - gm.miny; + const float xext = gm.maxx - gm.minx, zext = gm.maxz - gm.minz; + const bool flatWideGround = + (yext < 200.0f) && (xext > 500.0f) && (zext > 500.0f) && (gm.miny < 200.0f); + if (getenv("BT_GROUND_LOG")) + DEBUG_STREAM << "[visgnd] name-skip '" << rd->resourceName + << "' yext=" << yext << " xz=" << xext << "x" << zext + << " miny=" << gm.miny << " -> keepAsGround=" << (flatWideGround?1:0) + << "\n" << std::flush; + if (!flatWideGround) { ++skipped; continue; } + } + VgInst inst; inst.mesh = mi; inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2]; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 1e4ed43..9578090 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -4397,9 +4397,28 @@ void MECH_TARGET_SUBIDX(this) = -1; MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick } + else if (gBTTerrainEntity != 0) + { + // FIRE-AT-NOTHING (authentic, decomp part_013.c:7727): the pod fired + // freely at empty space -- the discharge gate is 0x388!=0 (the boresight + // always DESIGNATES a point, not a manual mech lock). When nothing pickable + // is hit (a level shot flies over the flat ground, or open sky), designate a + // MAX-RANGE point using the world sentinel so the weapon discharges to max + // range. Sentinel has no damage-zone table -> no aimed-zone damage, no lock + // ring (mechs only), per HudSimulation part_013.c:5620. + MECH_TARGET_ENTITY(this) = gBTTerrainEntity; + MECH_TARGET_SUBIDX(this) = -1; + Point3D farPt; + farPt.x = rs[0] + rd[0] * 1200.0f; // 1200 = HUD range default (0x44960000) + farPt.y = rs[1] + rd[1] * 1200.0f; + farPt.z = rs[2] + rd[2] * 1200.0f; + MECH_TARGET_POS(this) = farPt; + targetReticle.targetEntity = gBTTerrainEntity; + targetReticle.rayIntersection = farPt; + } else { - MECH_TARGET_ENTITY(this) = 0; // sky: no target, no discharge + MECH_TARGET_ENTITY(this) = 0; // no world sentinel -> no target MECH_TARGET_SUBIDX(this) = -1; } } @@ -4835,7 +4854,8 @@ void DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")" << (victim != 0 ? " MECH under boresight (aimed)" : (pickTarget != 0 ? " terrain downrange (beam at scenery)" - : " sky (no target, no discharge)")) + : (MECH_TARGET_ENTITY(this) != 0 ? " fire-at-nothing (max-range designate)" + : " no target (no world sentinel)"))) << " range=" << range << (anyWeaponInRange ? " IN WEAPON RANGE" : "") << " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround