@@ -431,6 +431,9 @@ struct native DebugAoEResult
431431var array <DebugAoEResult > DebugAoEList ;
432432var int CurrAoEIndex ;
433433
434+ // HL-Docs: ref:AIHazardEscape; issue:1559; tags:tactical
435+ var bool bNeedsHazardEscape ;
436+
434437// Generic ability selector for units that cannot be given a custom behavior tree.
435438// This should be used when the AI has no idea what abilities this unit has. (Shadow/Clone units, etc)
436439// This could potentially be used as a fallback BT as well.
@@ -1527,6 +1530,14 @@ simulated function BT_StartGetDestinations(bool bFiltered=false, bool bSkipBuild
15271530 DebugTileScores .AddItem (DebugScore );
15281531
15291532 m_kUnit .m_kReachableTilesCache .UpdateTileCacheIfNeeded ();
1533+
1534+ // HL-Docs: feature:AIHazardEscape; issue:1559
1535+ // Allows AI units to escape when surrounded by hazardous world effects (poison, fire, acid).
1536+ // When the normal pathfinding cache is empty due to hazards blocking all tiles,
1537+ // the AI gathers nearby non-hazardous tiles (3-5 tiles away), scores them using the standard tactical scoring system,
1538+ // and paths through hazards using BuildNonUnitPath to reach the best-scoring safe destination.
1539+ bNeedsHazardEscape = (m_kUnit .m_kReachableTilesCache .CoverDestinations .Length == 0 && class 'XComPath' .static .TileContainsHazard (UnitState , UnitState .TileLocation ));
1540+
15301541 // Special case for grapple movement.
15311542 if ( m_kCurrMoveRestriction .bIsGrappleMove )
15321543 {
@@ -1557,6 +1568,21 @@ simulated function BT_StartGetDestinations(bool bFiltered=false, bool bSkipBuild
15571568 AddTileToProcess (kTileScore );
15581569 }
15591570 }
1571+ else if (bNeedsHazardEscape )
1572+ {
1573+ // HL-Docs: ref:AIHazardEscape; issue:1559
1574+ // placed before CoverDestinations check because m_kReachableTilesCache will be empty
1575+ AllTiles = GatherHazardEscapeTiles ();
1576+ foreach AllTiles (kTile )
1577+ {
1578+ Position = WorldData .GetPositionFromTileCoordinates (kTile );
1579+ CoverPoint = EmptyCover ;
1580+ WorldData .GetCoverPointAtFloor (Position , CoverPoint );
1581+ kTileScore = InitMoveTileData (kTile , CoverPoint );
1582+ AddTileToProcess (kTileScore );
1583+ }
1584+ `Log ("Unit at (" $UnitState .TileLocation .X @ UnitState .TileLocation .Y @ UnitState .TileLocation .Z$ ") - found " $m_arrTilesToProcess .Length$ " tiles" ,,'BT_StartGetDestinations' );
1585+ }
15601586 else if ( !ShouldAvoidTilesWithCover () && !UnitState .IsCivilian () && !IsMeleeMove () && m_arrMoveWeightProfile [CurrMoveType ].fCoverWeight > 0.0f )
15611587 {
15621588 foreach m_kUnit .m_kReachableTilesCache .CoverDestinations (Position )
@@ -1792,6 +1818,41 @@ function BT_IgnoreHazards( bool bIgnore=true )
17921818 bIgnoreHazards = bIgnore ;
17931819}
17941820
1821+ // HL-Docs: ref:AIHazardEscape; issue:1559
1822+ // Gather nearby non-hazardous tiles for escape pathing then let BT_StepProcessDestinations score them
1823+ function array <TTile > GatherHazardEscapeTiles ()
1824+ {
1825+ local TTile TestTile ;
1826+ local array <TTile > EscapeTiles ;
1827+ local int dx , dy ;
1828+ local int MinRadius , MaxRadius ;
1829+
1830+ MinRadius = 3 ;
1831+ MaxRadius = 5 ;
1832+
1833+ // Check all tiles within radius range and return non-hazardous ones
1834+ for (dx = -MaxRadius ; dx <= MaxRadius ; ++dx )
1835+ {
1836+ for (dy = -MaxRadius ; dy <= MaxRadius ; ++dy )
1837+ {
1838+ TestTile = UnitState .TileLocation ;
1839+ TestTile .X += dx ;
1840+ TestTile .Y += dy ;
1841+
1842+ // Skip tiles too close (within MinRadius)
1843+ if (abs (dx ) < MinRadius && abs (dy ) < MinRadius )
1844+ continue ;
1845+
1846+ if (!class 'XComPath' .static .TileContainsHazard (UnitState , TestTile ))
1847+ {
1848+ EscapeTiles .AddItem (TestTile );
1849+ }
1850+ }
1851+ }
1852+
1853+ return EscapeTiles ;
1854+ }
1855+
17951856function BT_IncludeAlliesAsMeleeTargets ()
17961857{
17971858 bIncludeAlliesAsMeleeTargets = true ;
@@ -1928,8 +1989,10 @@ simulated function BT_StepProcessDestinations()
19281989 DebugTileScores [DebugIndex ].Location = vLoc ;
19291990 }
19301991
1992+ // HL-Docs: ref:AIHazardEscape; issue:1559
1993+ // m_kReachableTilesCache will be empty if we are surrounded by a hazard
19311994 //See if this tile is reachable
1932- if ( bValid && !m_kCurrMoveRestriction .bIsGrappleMove && !m_kUnit .m_kReachableTilesCache .IsTileReachable (kTileData .kTile ) )
1995+ if ( bValid && !bNeedsHazardEscape && ! m_kCurrMoveRestriction .bIsGrappleMove && !m_kUnit .m_kReachableTilesCache .IsTileReachable (kTileData .kTile ) )
19331996 {
19341997 if (bLogTacticalDestinationIteration )
19351998 {
@@ -1968,7 +2031,7 @@ simulated function BT_StepProcessDestinations()
19682031 }
19692032 else
19702033 {
1971- `LogAIBT ("Processed Destinations-" $m_arrMoveWeightProfile [CurrMoveType ].Profile$ "\n " $DebugBTScratchText );
2034+ `Log ("Processed Destinations-" $m_arrMoveWeightProfile [CurrMoveType ].Profile$ "\n " $DebugBTScratchText ,, 'AIBT' );
19722035 bBTUpdatingTacticalDestinations = false ;
19732036 break ;
19742037 }
@@ -8566,6 +8629,7 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
85668629 local bool bPathFailed;
85678630 local TTile kTileDest;
85688631 local array<PathPoint> PathPoints;
8632+ local array<ETraversalType> AllowedTraversals;
85698633
85708634 bPathFailed=false;
85718635
@@ -8585,7 +8649,9 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
85858649 bPathFailed = true;
85868650 }
85878651
8588- if (!bPathFailed)
8652+ // HL-Docs: ref:AIHazardEscape; issue:1559
8653+ // IsTileReachable will always return false if surrounded by hazards
8654+ if (!bPathFailed && !bNeedsHazardEscape)
85898655 {
85908656 bPathFailed = !m_kUnit.m_kReachableTilesCache.IsTileReachable(kTileDest);
85918657 if (bPathFailed)
@@ -8594,24 +8660,49 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
85948660 }
85958661 }
85968662
8597- if( !bPathFailed || bForcePathIfUnreachable )
8598- {
8663+ if( !bPathFailed || bForcePathIfUnreachable || bNeedsHazardEscape )
8664+ {
85998665 if (XGAIBehavior_Civilian(self) != none)
86008666 {
86018667 XGAIBehavior_Civilian(self).m_iMoveTimeStart = WorldInfo.TimeSeconds;
86028668 }
86038669
8604- m_kUnit.m_kReachableTilesCache.BuildPathToTile(kTileDest, Path);
8605- if( bForcePathIfUnreachable && Path.Length < 2 )
8670+ // HL-Docs: ref:AIHazardEscape; issue:1559
8671+ // Use BuildNonUnitPath for hazard escape to ensure the unit can path out of hazards
8672+ if (bNeedsHazardEscape)
8673+ {
8674+ // Build allowed traversals from unit's character template
8675+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Normal) AllowedTraversals.AddItem(eTraversal_Normal);
8676+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_ClimbOver) AllowedTraversals.AddItem(eTraversal_ClimbOver);
8677+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_ClimbOnto) AllowedTraversals.AddItem(eTraversal_ClimbOnto);
8678+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_ClimbLadder) AllowedTraversals.AddItem(eTraversal_ClimbLadder);
8679+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_DropDown) AllowedTraversals.AddItem(eTraversal_DropDown);
8680+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Grapple) AllowedTraversals.AddItem(eTraversal_Grapple);
8681+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Landing) AllowedTraversals.AddItem(eTraversal_Landing);
8682+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_BreakWindow) AllowedTraversals.AddItem(eTraversal_BreakWindow);
8683+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_KickDoor) AllowedTraversals.AddItem(eTraversal_KickDoor);
8684+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_JumpUp) AllowedTraversals.AddItem(eTraversal_JumpUp);
8685+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_WallClimb) AllowedTraversals.AddItem(eTraversal_WallClimb);
8686+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_BreakWall) AllowedTraversals.AddItem(eTraversal_BreakWall);
8687+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Launch) AllowedTraversals.AddItem(eTraversal_Launch);
8688+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Flying) AllowedTraversals.AddItem(eTraversal_Flying);
8689+ if (UnitState.GetMyTemplate().bCanUse_eTraversal_Land) AllowedTraversals.AddItem(eTraversal_Land);
8690+ class'X2PathSolver'.static.BuildNonUnitPath(UnitState.TileLocation, kTileDest, AllowedTraversals, Path);
8691+ }
8692+ else
86068693 {
8607- bPathFailed = false;
8608- class'X2PathSolver'.static.BuildPath(UnitState, UnitState.TileLocation, kTileDest, Path);
8609- // get the path points
8610- class'X2PathSolver'.static.GetPathPointsFromPath(UnitState, Path, PathPoints);
8611- // make the flight path nice and smooth
8612- class'XComPath'.static.PerformStringPulling(m_kUnit, PathPoints);
8613- // Reinsert into our array.
8614- class'XComPath'.static.GetPathTileArray(PathPoints, Path);
8694+ m_kUnit.m_kReachableTilesCache.BuildPathToTile(kTileDest, Path);
8695+ if( bForcePathIfUnreachable && Path.Length < 2 )
8696+ {
8697+ bPathFailed = false;
8698+ class'X2PathSolver'.static.BuildPath(UnitState, UnitState.TileLocation, kTileDest, Path);
8699+ // get the path points
8700+ class'X2PathSolver'.static.GetPathPointsFromPath(UnitState, Path, PathPoints);
8701+ // make the flight path nice and smooth
8702+ class'XComPath'.static.PerformStringPulling(m_kUnit, PathPoints);
8703+ // Reinsert into our array.
8704+ class'XComPath'.static.GetPathTileArray(PathPoints, Path);
8705+ }
86158706 }
86168707
86178708 if (Path.Length < 2)
0 commit comments