@@ -282,6 +282,8 @@ void CNavMeshAccessor::scanGrid()
282282 }
283283
284284 detectConnectOffset ();
285+ detectBlockedOffset ();
286+ detectAvoidanceObstacleOffset ();
285287
286288 m_teleportLinks.clear ();
287289
@@ -488,6 +490,67 @@ bool CNavMeshAccessor::detectConnectOffset()
488490 return false ;
489491}
490492
493+ // m_isBlocked[2] is at offset 0x36 (54) in the SDK CNavAreaCriticalData struct.
494+ // The accessor adds +4 for the vtable, so the arena-space offset is 0x3A (58).
495+ // per the SDK comment [7/24/2008 tom], so it should be stable across engine builds.
496+ bool CNavMeshAccessor::detectBlockedOffset ()
497+ {
498+ m_iBlockedOffset = -1 ;
499+ if (m_Areas.size () < 4 ) return false ;
500+ int good = 0 ;
501+ for (int s = 0 ; s < 8 && (size_t )s < m_Areas.size (); s++)
502+ {
503+ unsigned char *area = m_Areas[s];
504+ if (!area) continue ;
505+ unsigned char b0 = *(unsigned char *)(area + 0x3A );
506+ unsigned char b1 = *(unsigned char *)(area + 0x3B );
507+ if ((b0 <= 1 ) && (b1 <= 1 ))
508+ good++;
509+ }
510+ if (good >= 4 )
511+ {
512+ m_iBlockedOffset = 0x3A ;
513+ return true ;
514+ }
515+ return false ;
516+ }
517+
518+ // m_avoidanceObstacleHeight is a float at the end of CNavArea's non-critical
519+ // data. The exact offset varies between builds, so we probe a range.
520+ bool CNavMeshAccessor::detectAvoidanceObstacleOffset ()
521+ {
522+ m_iAvoidanceObstacleOffset = -1 ;
523+ if (m_Areas.size () < 4 ) return false ;
524+
525+ const int kMinOff = 0xA8 , kMaxOff = 0x2FC ;
526+ int bestOff = -1 ;
527+ float bestScore = 0 .0f ;
528+
529+ for (int off = kMinOff ; off <= kMaxOff ; off += 4 )
530+ {
531+ int zeroCnt = 0 , posCnt = 0 ;
532+ for (int s = 0 ; s < 16 && (size_t )s < m_Areas.size (); s++)
533+ {
534+ unsigned char *area = m_Areas[s];
535+ if (!area) continue ;
536+ float v = *(float *)(area + off);
537+ if (!(v >= 0 .0f && v <= 4096 .0f )) { zeroCnt = posCnt = -99 ; break ; }
538+ if (v == 0 .0f ) zeroCnt++;
539+ else if (v > 0 .0f ) posCnt++;
540+ }
541+ if (zeroCnt < 0 ) continue ;
542+ float score = (float )zeroCnt * 0 .5f + (float )posCnt * 1 .5f ;
543+ if (score > bestScore) { bestScore = score; bestOff = off; }
544+ }
545+
546+ if (bestOff >= 0 && bestScore >= 10 .0f )
547+ {
548+ m_iAvoidanceObstacleOffset = bestOff;
549+ return true ;
550+ }
551+ return false ;
552+ }
553+
491554unsigned char *CNavMeshAccessor::getAreaByIndex (int i) const
492555{
493556 if (i < 0 || i >= (int )m_Areas.size ()) return nullptr ;
@@ -726,6 +789,75 @@ bool CNavMeshNavigator::workRoute(Vector vFrom, Vector vTo, bool *bFail,
726789 float stepCost = dx * dx + dy * dy + dz * dz;
727790 if (nCenter[2 ] < vCurCenter.z - 128 .0f )
728791 stepCost += (vCurCenter.z - nCenter[2 ]) * (vCurCenter.z - nCenter[2 ]) * 3 .0f ;
792+
793+ // --- Blocked-area check (team-specific) ---
794+ {
795+ int blkOff = m_pAccessor->getBlockedOffset ();
796+ if (blkOff >= 0 )
797+ {
798+ int team = m_pBot ? m_pBot->getTeam () : 0 ;
799+ if (team >= 0 && team < 2 )
800+ {
801+ unsigned char blocked = *(unsigned char *)((unsigned char *)neighbor + blkOff + team);
802+ if (blocked) continue ;
803+ }
804+ }
805+ }
806+
807+ // --- Avoidance-obstacle penalty ---
808+ {
809+ int obsOff = m_pAccessor->getAvoidanceObstacleOffset ();
810+ if (obsOff >= 0 )
811+ {
812+ float obsH = *(float *)((unsigned char *)neighbor + obsOff);
813+ if (obsH > 0 .0f && obsH <= 512 .0f )
814+ stepCost += obsH * 10 .0f ;
815+ }
816+ }
817+
818+ // --- Class-aware JUMP connection cost ---
819+ int curAttr = *(int *)((unsigned char *)current + NAV_M_ATTR );
820+ if (curAttr & NAV_ATTR_JUMP )
821+ {
822+ float zDelta = fabsf (nCenter[2 ] - vCurCenter.z );
823+ float maxJump = 72 .0f ; // default baseline
824+ float enhCostMul = 2 .0f ;
825+ if (m_pBot && m_pBot->isTF2 ())
826+ {
827+ CBotTF2 *pTF2 = (CBotTF2 *)m_pBot;
828+ int tfClass = pTF2->getClass ();
829+ if (tfClass == TF_CLASS_SCOUT )
830+ maxJump = 108 .0f ;
831+ else if (tfClass == TF_CLASS_SOLDIER && pTF2->getHealthPercent () > 0 .5f )
832+ maxJump = 128 .0f ;
833+ else if (tfClass == TF_CLASS_DEMOMAN && rcbot_demo_jump && rcbot_demo_jump->GetInt ())
834+ maxJump = 128 .0f ;
835+ else
836+ {
837+ if (rcbot_navmesh_max_jump_height)
838+ maxJump = rcbot_navmesh_max_jump_height->GetFloat ();
839+ extern ConVar *rcbot_navmesh_max_jump_height;
840+ if (rcbot_navmesh_max_jump_height)
841+ maxJump = rcbot_navmesh_max_jump_height->GetFloat ();
842+ }
843+ }
844+ if (zDelta > maxJump)
845+ continue ; // too high for this class
846+ if (zDelta > 72 .0f )
847+ stepCost *= (zDelta / 72 .0f ) * enhCostMul;
848+ }
849+
850+ // --- NAV_MESH_AVOID penalty ---
851+ if (curAttr & NAV_MESH_AVOID )
852+ stepCost *= 2 .0f ;
853+
854+ // --- Frustration penalty ---
855+ {
856+ auto fit = m_mapAreaFrustration.find (neighbor);
857+ if (fit != m_mapAreaFrustration.end () && fit->second > 0 .01f )
858+ stepCost *= (1 .0f + fit->second );
859+ }
860+
729861 float tentativeG = gScore [current] + stepCost;
730862
731863 auto gIt = gScore .find (neighbor);
@@ -1083,6 +1215,19 @@ void CNavMeshNavigator::optimizePath()
10831215 if (!isPotentiallyTraversable (posA, posC))
10841216 continue ;
10851217
1218+ // Obstacle check: trace from waist height A→C.
1219+ // If a vertical obstacle sits between them, this shortcut would
1220+ // pass through a crate or low wall — reject.
1221+ {
1222+ CTraceFilterWorldAndPropsOnly trF;
1223+ Vector vTraceA = posA + Vector (0 ,0 ,18 );
1224+ Vector vTraceC = posC + Vector (0 ,0 ,18 );
1225+ CBotGlobals::traceLine (vTraceA, vTraceC, MASK_PLAYERSOLID , &trF);
1226+ trace_t *tr = CBotGlobals::getTraceResult ();
1227+ if (tr && tr->fraction < 0 .75f && tr->plane .normal .z < 0 .5f )
1228+ continue ;
1229+ }
1230+
10861231 // Remove node B at index i-1
10871232 m_route.erase (m_route.begin () + (i - 1 ));
10881233 removed = true ;
@@ -1269,6 +1414,22 @@ bool CNavMeshNavigator::hasNextPoint() { return !m_route.empty(); }
12691414void CNavMeshNavigator::updatePosition ()
12701415{
12711416 if (!m_pBot) return ;
1417+
1418+ // Frustration decay: reduce all area frustration scores by 10% per frame.
1419+ // Prune entries that have decayed below 0.01.
1420+ {
1421+ auto it = m_mapAreaFrustration.begin ();
1422+ while (it != m_mapAreaFrustration.end ())
1423+ {
1424+ it->second *= 0 .9f ;
1425+ if (it->second < 0 .01f )
1426+ it = m_mapAreaFrustration.erase (it);
1427+ else
1428+ ++it;
1429+ }
1430+ }
1431+
1432+ if (!m_pAccessor || !m_pAccessor->ready ()) { freeMapMemory (); return ; }
12721433 if (!m_pAccessor || !m_pAccessor->ready ()) { freeMapMemory (); return ; }
12731434
12741435 // Phase 1: Escape mode (handles its own early-return if active)
@@ -1420,6 +1581,14 @@ void CNavMeshNavigator::updatePosition()
14201581 else
14211582 {
14221583 m_iConsecutiveHits++;
1584+ // Frustration memory: when stuck hitting walls in this area,
1585+ // increment its cost for future A* searches.
1586+ if (m_iConsecutiveHits >= 8 && m_pCurrentArea)
1587+ {
1588+ float &scr = m_mapAreaFrustration[m_pCurrentArea];
1589+ scr += 1 .0f ;
1590+ if (scr > 5 .0f ) scr = 5 .0f ;
1591+ }
14231592 if (m_iConsecutiveHits >= 60 )
14241593 {
14251594 void *stuckDst = (!m_route.empty ()) ? m_route.back ().area : nullptr ;
0 commit comments