Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ var() TAppearance kAppearance;
/**
* THESE VARS HAVE BEEN COPIED FROM TCharacter AND ARE NOT REFACTORED YET
*/
var() protected int aTraversals[ETraversalType.EnumCount]<FGDEIgnore=true>;
// HL-Docs: ref:AIHazardEscape; issue:1559
var() /* protected */ int aTraversals[ETraversalType.EnumCount]<FGDEIgnore=true>;

/**
* END TCharacter VARS
Expand Down
126 changes: 110 additions & 16 deletions X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ struct native DebugAoEResult
var array<DebugAoEResult> DebugAoEList;
var int CurrAoEIndex;


// Generic ability selector for units that cannot be given a custom behavior tree.
// This should be used when the AI has no idea what abilities this unit has. (Shadow/Clone units, etc)
// This could potentially be used as a fallback BT as well.
Expand Down Expand Up @@ -1527,6 +1528,7 @@ simulated function BT_StartGetDestinations(bool bFiltered=false, bool bSkipBuild
DebugTileScores.AddItem(DebugScore);

m_kUnit.m_kReachableTilesCache.UpdateTileCacheIfNeeded();

// Special case for grapple movement.
if ( m_kCurrMoveRestriction.bIsGrappleMove )
{
Expand Down Expand Up @@ -1557,6 +1559,21 @@ simulated function BT_StartGetDestinations(bool bFiltered=false, bool bSkipBuild
AddTileToProcess(kTileScore);
}
}
// HL-Docs: ref:AIHazardEscape; issue:1559
// placed before CoverDestinations check because m_kReachableTilesCache will be empty
else if (NeedsHazardEscape())
{
AllTiles = GatherHazardEscapeTiles();
foreach AllTiles(kTile)
{
Position = WorldData.GetPositionFromTileCoordinates(kTile);
CoverPoint = EmptyCover;
WorldData.GetCoverPointAtFloor(Position, CoverPoint);
kTileScore = InitMoveTileData(kTile, CoverPoint);
AddTileToProcess(kTileScore);
}
`Log("Unit at ("$UnitState.TileLocation.X@UnitState.TileLocation.Y@UnitState.TileLocation.Z$") - found "$m_arrTilesToProcess.Length$" tiles",,'BT_StartGetDestinations');
}
else if( !ShouldAvoidTilesWithCover() && !UnitState.IsCivilian() && !IsMeleeMove() && m_arrMoveWeightProfile[CurrMoveType].fCoverWeight > 0.0f)
{
foreach m_kUnit.m_kReachableTilesCache.CoverDestinations(Position)
Expand Down Expand Up @@ -1792,6 +1809,51 @@ function BT_IgnoreHazards( bool bIgnore=true )
bIgnoreHazards = bIgnore;
}

// HL-Docs: feature:AIHazardEscape; issue:1559; tags:tactical
// Allows AI units to escape when surrounded by hazardous world effects (poison, fire, acid).
// When the normal pathfinding cache is empty due to hazards blocking all tiles,
// the AI gathers nearby non-hazardous tiles (3-5 tiles away), scores them using the standard tactical scoring system,
// and paths through hazards using BuildNonUnitPath to reach the best-scoring safe destination.
function bool NeedsHazardEscape()
{
return (m_kUnit.m_kReachableTilesCache.CoverDestinations.Length == 0 && class'XComPath'.static.TileContainsHazard(UnitState, UnitState.TileLocation));
}

// HL-Docs: ref:AIHazardEscape; issue:1559
// Gather nearby non-hazardous tiles for escape pathing then let BT_StepProcessDestinations score them
function array<TTile> GatherHazardEscapeTiles()
{
local TTile TestTile;
local array<TTile> EscapeTiles;
local int dx, dy;
local int MinRadius, MaxRadius;

MinRadius = 3;
MaxRadius = 5;

// Check all tiles within radius range and return non-hazardous ones
for (dx = -MaxRadius; dx <= MaxRadius; ++dx)
{
for (dy = -MaxRadius; dy <= MaxRadius; ++dy)
{
TestTile = UnitState.TileLocation;
TestTile.X += dx;
TestTile.Y += dy;

// Skip tiles too close (within MinRadius)
if (abs(dx) < MinRadius && abs(dy) < MinRadius)
continue;

if (!class'XComPath'.static.TileContainsHazard(UnitState, TestTile))
{
EscapeTiles.AddItem(TestTile);
}
}
}

return EscapeTiles;
}

function BT_IncludeAlliesAsMeleeTargets()
{
bIncludeAlliesAsMeleeTargets = true;
Expand Down Expand Up @@ -1928,8 +1990,10 @@ simulated function BT_StepProcessDestinations()
DebugTileScores[DebugIndex].Location = vLoc;
}

// HL-Docs: ref:AIHazardEscape; issue:1559
// m_kReachableTilesCache will be empty if we are surrounded by a hazard
//See if this tile is reachable
if ( bValid && !m_kCurrMoveRestriction.bIsGrappleMove && !m_kUnit.m_kReachableTilesCache.IsTileReachable(kTileData.kTile) )
if ( bValid && !NeedsHazardEscape() && !m_kCurrMoveRestriction.bIsGrappleMove && !m_kUnit.m_kReachableTilesCache.IsTileReachable(kTileData.kTile) )
{
if (bLogTacticalDestinationIteration)
{
Expand Down Expand Up @@ -1968,7 +2032,7 @@ simulated function BT_StepProcessDestinations()
}
else
{
`LogAIBT("Processed Destinations-"$m_arrMoveWeightProfile[CurrMoveType].Profile$"\n"$DebugBTScratchText);
`Log("Processed Destinations-"$m_arrMoveWeightProfile[CurrMoveType].Profile$"\n"$DebugBTScratchText,,'AIBT');
bBTUpdatingTacticalDestinations = false;
break;
}
Expand Down Expand Up @@ -8566,9 +8630,10 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
local bool bPathFailed;
local TTile kTileDest;
local array<PathPoint> PathPoints;
local array<ETraversalType> AllowedTraversals;
local bool bNeedsHazardEscape;

bPathFailed=false;

kTileDest = `XWORLD.GetTileCoordinatesFromPosition(vDestination);

if (kTileDest.X == UnitState.TileLocation.X && kTileDest.Y == UnitState.TileLocation.Y && abs(UnitState.TileLocation.Z-kTileDest.Z) <= 3)
Expand All @@ -8585,7 +8650,10 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
bPathFailed = true;
}

if (!bPathFailed)
// HL-Docs: ref:AIHazardEscape; issue:1559
// IsTileReachable will always return false if surrounded by hazards, so skip this check
bNeedsHazardEscape = NeedsHazardEscape();
if (!bPathFailed && !bNeedsHazardEscape)
{
bPathFailed = !m_kUnit.m_kReachableTilesCache.IsTileReachable(kTileDest);
if (bPathFailed)
Expand All @@ -8594,24 +8662,50 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai
}
}

if( !bPathFailed || bForcePathIfUnreachable )
{
if( !bPathFailed || bForcePathIfUnreachable || bNeedsHazardEscape )
{
if (XGAIBehavior_Civilian(self) != none)
{
XGAIBehavior_Civilian(self).m_iMoveTimeStart = WorldInfo.TimeSeconds;
}

m_kUnit.m_kReachableTilesCache.BuildPathToTile(kTileDest, Path);
if( bForcePathIfUnreachable && Path.Length < 2 )
// HL-Docs: ref:AIHazardEscape; issue:1559
// Use BuildNonUnitPath to path through hazards with runtime traversal capabilities
if (bNeedsHazardEscape)
{
// Build allowed traversals from unit's runtime traversal capabilities
if (UnitState.aTraversals[eTraversal_Normal] > 0) AllowedTraversals.AddItem(eTraversal_Normal);
if (UnitState.aTraversals[eTraversal_ClimbOver] > 0) AllowedTraversals.AddItem(eTraversal_ClimbOver);
if (UnitState.aTraversals[eTraversal_ClimbOnto] > 0) AllowedTraversals.AddItem(eTraversal_ClimbOnto);
if (UnitState.aTraversals[eTraversal_ClimbLadder] > 0) AllowedTraversals.AddItem(eTraversal_ClimbLadder);
if (UnitState.aTraversals[eTraversal_DropDown] > 0) AllowedTraversals.AddItem(eTraversal_DropDown);
if (UnitState.aTraversals[eTraversal_Grapple] > 0) AllowedTraversals.AddItem(eTraversal_Grapple);
if (UnitState.aTraversals[eTraversal_Landing] > 0) AllowedTraversals.AddItem(eTraversal_Landing);
if (UnitState.aTraversals[eTraversal_BreakWindow] > 0) AllowedTraversals.AddItem(eTraversal_BreakWindow);
if (UnitState.aTraversals[eTraversal_KickDoor] > 0) AllowedTraversals.AddItem(eTraversal_KickDoor);
if (UnitState.aTraversals[eTraversal_JumpUp] > 0) AllowedTraversals.AddItem(eTraversal_JumpUp);
if (UnitState.aTraversals[eTraversal_WallClimb] > 0) AllowedTraversals.AddItem(eTraversal_WallClimb);
if (UnitState.aTraversals[eTraversal_Phasing] > 0) AllowedTraversals.AddItem(eTraversal_Phasing);
if (UnitState.aTraversals[eTraversal_BreakWall] > 0) AllowedTraversals.AddItem(eTraversal_BreakWall);
if (UnitState.aTraversals[eTraversal_Launch] > 0) AllowedTraversals.AddItem(eTraversal_Launch);
if (UnitState.aTraversals[eTraversal_Flying] > 0) AllowedTraversals.AddItem(eTraversal_Flying);
if (UnitState.aTraversals[eTraversal_Land] > 0) AllowedTraversals.AddItem(eTraversal_Land);
class'X2PathSolver'.static.BuildNonUnitPath(UnitState.TileLocation, kTileDest, AllowedTraversals, Path);
}
else
{
bPathFailed = false;
class'X2PathSolver'.static.BuildPath(UnitState, UnitState.TileLocation, kTileDest, Path);
// get the path points
class'X2PathSolver'.static.GetPathPointsFromPath(UnitState, Path, PathPoints);
// make the flight path nice and smooth
class'XComPath'.static.PerformStringPulling(m_kUnit, PathPoints);
// Reinsert into our array.
class'XComPath'.static.GetPathTileArray(PathPoints, Path);
m_kUnit.m_kReachableTilesCache.BuildPathToTile(kTileDest, Path);
if( bForcePathIfUnreachable && Path.Length < 2 )
{
bPathFailed = false;
class'X2PathSolver'.static.BuildPath(UnitState, UnitState.TileLocation, kTileDest, Path);
// get the path points
class'X2PathSolver'.static.GetPathPointsFromPath(UnitState, Path, PathPoints);
// make the flight path nice and smooth
class'XComPath'.static.PerformStringPulling(m_kUnit, PathPoints);
// Reinsert into our array.
class'XComPath'.static.GetPathTileArray(PathPoints, Path);
}
}

if (Path.Length < 2)
Expand Down