Skip to content

Commit 82ec365

Browse files
WhakomaticWhakomatic
authored andcommitted
AIHazardEscape; issue:1559
1 parent 1e87269 commit 82ec365

1 file changed

Lines changed: 106 additions & 15 deletions

File tree

X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,9 @@ struct native DebugAoEResult
431431
var array<DebugAoEResult> DebugAoEList;
432432
var 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+
17951856
function 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

Comments
 (0)