Combat: RESTORE authentic fire-at-nothing + non-mech range axis (task #50 / Discord report)
Determined the authentic behavior from the decomp (HudSimulation + Emitter,
part_013.c:5619-5670 / 7689-7778) and fixed two regressions the players hit:
(A) You could only fire when a MECH was locked (couldn't fire at nothing).
(B) Buildings/structures/ground stopped moving the range axis; only mechs did.
AUTHENTIC (T1): a weapon discharges iff the target slot mech+0x388 != 0
(Emitter::FireWeapon FUN_004bace8:7727), and 0x388 is whatever the BORESIGHT
designates -- mech OR world geometry -- NOT a manual mech lock. The RANGE readout
(HUD+0x1ec) moves for ANY designated target (mech OR structure/ground); the LOCK
ring is mech-only (target has a damage-zone table, HudSim :5619). So the pod
'fired freely at nothing' and buildings moved the range caret.
ROOT CAUSES (both content-triggered, not a targeting-code change):
1. arena1's ONLY class-42 world entity is the 10000x10000 FLAT GROUND plane at
y=0, MISNAMED 'sky' -- SkippedName filtered it out by name, so BuildTables
ingested 0 geometry and gBTTerrainEntity stayed null -> nothing groundable ->
mech-only targeting. (LAST.EGG rewrite ~bb795e2 lost whatever terrain the
working scene had.)
2. A boresight that hit nothing set MECH_TARGET_ENTITY=0 -> no discharge.
FIXES:
- btvisgnd.cpp: geometry-aware skip -- keep a wide, near-flat, near-ground plane
even if name-skipped (it is the arena floor); still skip true domes/backdrops.
Adds mesh y-bounds. arena1 now ingests 1 ground instance (was 0).
- btl4vid.cpp: capture ANY world-geometry entity as the non-mech pick sentinel
(the default render case is world-only), not only Terrain-derived ones.
- mech4.cpp: FIRE-AT-NOTHING -- when nothing pickable is hit, designate a
max-range point (1200, the HUD default 0x44960000) along the boresight using
the world sentinel, so 0x388 != 0 and the weapon discharges (no zone damage /
no lock ring, since the sentinel has no damage-zone table -- mechs only).
Verified autonomous: arena1 ground ingested; [target] shows 'fire-at-nothing
(max-range designate)'; weapon discharges at empty space ([fire] explode
resolved); range axis (sShownRange->BTSetHudTargetRange) slides to the designated
distance. Diagnostic: BT_GROUND_LOG (world-geometry ingest + skip reasons).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
49d73dc8e2
commit
ee6e19e86e
@@ -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);
|
||||
|
||||
@@ -53,6 +53,7 @@ struct VgMesh
|
||||
{
|
||||
std::vector<VgTri> 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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user