diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Unit.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Unit.uc index e846734a2..3f9104f89 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Unit.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Unit.uc @@ -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]; +// HL-Docs: ref:AIHazardEscape; issue:1559 +var() /* protected */ int aTraversals[ETraversalType.EnumCount]; /** * END TCharacter VARS diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc index b7e1327d7..b50df85d1 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XGAIBehavior.uc @@ -431,6 +431,7 @@ struct native DebugAoEResult var array 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. @@ -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 ) { @@ -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) @@ -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 GatherHazardEscapeTiles() +{ + local TTile TestTile; + local array 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; @@ -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) { @@ -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; } @@ -8566,9 +8630,10 @@ simulated function bool MoveToPoint(vector vDestination, optional out string Fai local bool bPathFailed; local TTile kTileDest; local array PathPoints; + local array 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) @@ -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) @@ -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)