From 7512ae4aed72fa749d8963a13b93b8d1ba83ea37 Mon Sep 17 00:00:00 2001 From: furudee Date: Wed, 25 Dec 2024 01:42:18 +0200 Subject: [PATCH 1/2] Issue X2CommunityCore#1431 - Add X2Effect_Knockback and XComGameStateContext_Falling --- .../XComGame/Classes/X2Effect_Knockback.uc | 429 ++++++++++++++++++ .../Classes/XComGameStateContext_Falling.uc | 386 ++++++++++++++++ .../X2WOTCCommunityHighlander.x2proj | 6 + 3 files changed, 821 insertions(+) create mode 100644 X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc create mode 100644 X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc new file mode 100644 index 000000000..01345d41a --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc @@ -0,0 +1,429 @@ +//--------------------------------------------------------------------------------------- +// FILE: X2Effect_Knockback.uc +// AUTHOR: Ryan McFall -- 5/5/2015 +// PURPOSE: This effect implements a game mechanic that moves units based on incoming +// damage or attacks. This is done with a rag doll similar to what was done with +// kinetic strike in EW. +// +//--------------------------------------------------------------------------------------- +// Copyright (c) 2016 Firaxis Games, Inc. All rights reserved. +//--------------------------------------------------------------------------------------- +class X2Effect_Knockback extends X2Effect config(GameCore); + +struct KnockbackDistanceOverride +{ + var name OverrideReason; + var int NewKnockbackDistance_Meters; +}; + +/** Distance that the target will be thrown backwards, in meters */ +var() int KnockbackDistance; + +/** Used to step the knockback forward along the movement vector until either knock back distance is reached, or there are no more valid tiles*/ +var() private float IncrementalStepSize; + +/** If true, the knocked back unit will cause non fragile destruction ( like kinetic strike ) */ +var() bool bKnockbackDestroysNonFragile; + +/** Distance that the target will be thrown backwards, in meters */ +var() float OverrideRagdollFinishTimerSec; + +/** Knockback effects can happen on every attack or only killing attacks */ +var() bool OnlyOnDeath; + +var config float DefaultDamage; +var config float DefaultRadius; +var config array KnockbackDistanceOverrides; + +function name WasTargetPreviouslyDead(const out EffectAppliedData ApplyEffectParameters, XComGameState_BaseObject kNewTargetState, XComGameState NewGameState) +{ + // A unit that was dead before this game state should not get a knockback, they are already a corpse + local name AvailableCode; + local XComGameState_Unit TestUnitState; + local XComGameStateHistory History; + + AvailableCode = 'AA_Success'; + + History = `XCOMHISTORY; + + TestUnitState = XComGameState_Unit(History.GetGameStateForObjectID(kNewTargetState.ObjectID)); + if( (TestUnitState != none) && TestUnitState.IsDead() ) + { + return 'AA_UnitIsDead'; + } + + if( OnlyOnDeath ) + { + TestUnitState = XComGameState_Unit(kNewTargetState); + if( TestUnitState != None && (TestUnitState.IsAlive() || TestUnitState.IsIncapacitated()) ) + { + return 'AA_UnitIsAlive'; + } + } + + return AvailableCode; +} + +private function bool CanBeDestroyed(XComInteractiveLevelActor InteractiveActor, float DamageAmount) +{ + //make sure the knockback damage can destroy this actor. + //check the number of interaction points to prevent larger objects from being destroyed. + return InteractiveActor != none && DamageAmount >= InteractiveActor.Health && InteractiveActor.InteractionPoints.Length <= 8; +} + +private function int GetKnockbackDistance(XComGameStateContext_Ability AbilityContext, XComGameState_BaseObject kNewTargetState) +{ + local int UpdatedKnockbackDistance_Meters, ReasonIndex; + local XComGameState_Unit TargetUnitState; + local name UnitTypeName; + + UpdatedKnockbackDistance_Meters = KnockbackDistance; + + TargetUnitState = XComGameState_Unit(kNewTargetState); + if (TargetUnitState != none) + { + UnitTypeName = TargetUnitState.GetMyTemplate().CharacterGroupName; + } + + // For now, the only OverrideReason is CharacterGroupName. If otheres are desired, add extra checks here. + ReasonIndex = KnockbackDistanceOverrides.Find('OverrideReason', UnitTypeName); + + if (ReasonIndex != INDEX_NONE) + { + UpdatedKnockbackDistance_Meters = KnockbackDistanceOverrides[ReasonIndex].NewKnockbackDistance_Meters; + } + + return UpdatedKnockbackDistance_Meters; +} + +//Returns the list of tiles that the unit will pass through as part of the knock back. The last tile in the array is the final destination. +private function GetTilesEnteredArray(XComGameStateContext_Ability AbilityContext, XComGameState_BaseObject kNewTargetState, out array OutTilesEntered, out Vector OutAttackDirection, float DamageAmount, XComGameState NewGameState) +{ + local XComWorldData WorldData; + local XComGameState_Unit SourceUnit; + local XComGameState_Unit TargetUnit; + local Vector SourceLocation; + local Vector TargetLocation; + local Vector StartLocation; + local TTile TempTile, StartTile; + local TTile LastTempTile; + local Vector KnockbackToLocation; + local float StepDistance; + local Vector TestLocation; + local float TestDistanceUnits; + local TTile MoveToTile; + local XGUnit TargetVisualizer; + local XComUnitPawn TargetUnitPawn; + local Vector Extents; + local XComGameStateHistory History; + + local ActorTraceHitInfo TraceHitInfo; + local array Hits; + local Actor FloorTileActor; + + local X2AbilityTemplate AbilityTemplate; + local bool bCursorTargetFound; + local X2AbilityToHitCalc_StandardAim ToHitCalc; + + local int UpdatedKnockbackDistance_Meters; + local array TileUnits; + + WorldData = `XWORLD; + History = `XCOMHISTORY; + if(AbilityContext != none) + { + AbilityTemplate = class'XComGameState_Ability'.static.GetMyTemplateManager().FindAbilityTemplate(AbilityContext.InputContext.AbilityTemplateName); + + TargetUnit = XComGameState_Unit(kNewTargetState); + TargetUnit.GetKeystoneVisibilityLocation(StartTile); + TargetLocation = WorldData.GetPositionFromTileCoordinates(StartTile); + + ToHitCalc = X2AbilityToHitCalc_StandardAim(AbilityTemplate.AbilityToHitCalc); + if (ToHitCalc != none && ToHitCalc.bReactionFire) + { + //If this was reaction fire, just drop the unit where they are. The physics of their motion may move them a few tiles + WorldData.GetFloorTileForPosition(TargetLocation, MoveToTile, true); + OutTilesEntered.AddItem(MoveToTile); + } + else + { + if (AbilityTemplate != none && AbilityTemplate.AbilityTargetStyle.IsA('X2AbilityTarget_Cursor')) + { + //attack source is at cursor location + `assert( AbilityContext.InputContext.TargetLocations.Length > 0 ); + SourceLocation = AbilityContext.InputContext.TargetLocations[0]; + + TempTile = WorldData.GetTileCoordinatesFromPosition(SourceLocation); + SourceLocation = WorldData.GetPositionFromTileCoordinates(TempTile); + + //Need to produce a non-zero vector + bCursorTargetFound = (SourceLocation.X != TargetLocation.X || SourceLocation.Y != TargetLocation.Y); + } + + if (!bCursorTargetFound) + { + //attack source is from a Unit + SourceUnit = XComGameState_Unit(NewGameState.GetGameStateForObjectID(AbilityContext.InputContext.SourceObject.ObjectID)); + SourceUnit.GetKeystoneVisibilityLocation(TempTile); + SourceLocation = WorldData.GetPositionFromTileCoordinates(TempTile); + } + + OutAttackDirection = Normal(TargetLocation - SourceLocation); + OutAttackDirection.Z = 0.0f; + StartLocation = TargetLocation; + + UpdatedKnockbackDistance_Meters = GetKnockbackDistance(AbilityContext, kNewTargetState); + + KnockbackToLocation = StartLocation + (OutAttackDirection * float(UpdatedKnockbackDistance_Meters) * 64.0f); //Convert knockback distance to meters + + TargetVisualizer = XGUnit(History.GetVisualizer(TargetUnit.ObjectID)); + if( TargetVisualizer != None ) + { + TargetUnitPawn = TargetVisualizer.GetPawn(); + if( TargetUnitPawn != None ) + { + Extents.X = TargetUnitPawn.CylinderComponent.CollisionRadius; + Extents.Y = TargetUnitPawn.CylinderComponent.CollisionRadius; + Extents.Z = TargetUnitPawn.CylinderComponent.CollisionHeight; + } + } + + if( WorldData.GetAllActorsTrace(StartLocation, KnockbackToLocation, Hits, Extents) ) + { + foreach Hits(TraceHitInfo) + { + TempTile = WorldData.GetTileCoordinatesFromPosition(TraceHitInfo.HitLocation); + FloorTileActor = WorldData.GetFloorTileActor(TempTile); + + if( TraceHitInfo.HitActor == FloorTileActor ) + { + continue; + } + + if ((!CanBeDestroyed(XComInteractiveLevelActor(TraceHitInfo.HitActor), DamageAmount) && XComFracLevelActor(TraceHitInfo.HitActor) == none) || !bKnockbackDestroysNonFragile) + { + //We hit an indestructible object + KnockbackToLocation = TraceHitInfo.HitLocation + (-OutAttackDirection * 16.0f); //Scoot the hit back a bit and use that as the knockback location + break; + } + } + } + + //Walk in increments down the attack vector. We will stop if we can't find a floor, or have reached the knock back distance, or we encounter another unit. + TestDistanceUnits = VSize2D(KnockbackToLocation - StartLocation); + StepDistance = 0.0f; + OutTilesEntered.Length = 0; + LastTempTile = StartTile; + while (StepDistance < TestDistanceUnits) + { + TestLocation = StartLocation + (OutAttackDirection * StepDistance); + + if (!WorldData.GetFloorTileForPosition(TestLocation, TempTile, true)) + { + break; + } + + if (TempTile != StartTile) // don't check the start tile, since the target unit would be on it + { + TileUnits = WorldData.GetUnitsOnTile(TempTile); + if (TileUnits.Length > 0) + break; + } + + if (LastTempTile != TempTile) + { + OutTilesEntered.AddItem(TempTile); + LastTempTile = TempTile; + } + + StepDistance += IncrementalStepSize; + } + + //Move the target unit to the knockback location + if (OutTilesEntered.Length == 0 || OutTilesEntered[OutTilesEntered.Length - 1] != LastTempTile) + OutTilesEntered.AddItem(LastTempTile); + } + } +} + +simulated function ApplyEffectToWorld(const out EffectAppliedData ApplyEffectParameters, XComGameState NewGameState) +{ + local XComGameStateContext_Ability AbilityContext; + local XComGameState_BaseObject kNewTargetState; + local int Index; + local XComGameState_EnvironmentDamage DamageEvent; + local XComWorldData WorldData; + local TTile HitTile; + local array TilesEntered; + local Vector AttackDirection; + local XComGameState_Item SourceItemStateObject; + local XComGameStateHistory History; + local X2WeaponTemplate WeaponTemplate; + local array Targets; + local StateObjectReference CurrentTarget; + local XComGameState_Unit TargetUnit; + local TTile NewTileLocation; + local float KnockbackDamage; + local float KnockbackRadius; + local int EffectIndex, MultiTargetIndex; + local X2Effect_Knockback KnockbackEffect; + + AbilityContext = XComGameStateContext_Ability(NewGameState.GetContext()); + if(AbilityContext != none) + { + if (AbilityContext.InputContext.PrimaryTarget.ObjectID > 0) + { + // Check the Primary Target for a successful knockback + for (EffectIndex = 0; EffectIndex < AbilityContext.ResultContext.TargetEffectResults.Effects.Length; ++EffectIndex) + { + KnockbackEffect = X2Effect_Knockback(AbilityContext.ResultContext.TargetEffectResults.Effects[EffectIndex]); + if (KnockbackEffect != none) + { + if (AbilityContext.ResultContext.TargetEffectResults.ApplyResults[EffectIndex] == 'AA_Success') + { + Targets.AddItem(AbilityContext.InputContext.PrimaryTarget); + break; + } + } + } + } + + for (MultiTargetIndex = 0; MultiTargetIndex < AbilityContext.InputContext.MultiTargets.Length; ++MultiTargetIndex) + { + // Check the MultiTargets for a successful knockback + for (EffectIndex = 0; EffectIndex < AbilityContext.ResultContext.MultiTargetEffectResults[MultiTargetIndex].Effects.Length; ++EffectIndex) + { + KnockbackEffect = X2Effect_Knockback(AbilityContext.ResultContext.MultiTargetEffectResults[MultiTargetIndex].Effects[EffectIndex]); + if (KnockbackEffect != none) + { + if (AbilityContext.ResultContext.MultiTargetEffectResults[MultiTargetIndex].ApplyResults[EffectIndex] == 'AA_Success') + { + Targets.AddItem(AbilityContext.InputContext.MultiTargets[MultiTargetIndex]); + break; + } + } + } + } + + foreach Targets(CurrentTarget) + { + History = `XCOMHISTORY; + SourceItemStateObject = XComGameState_Item(History.GetGameStateForObjectID(ApplyEffectParameters.ItemStateObjectRef.ObjectID)); + if (SourceItemStateObject != None) + WeaponTemplate = X2WeaponTemplate(SourceItemStateObject.GetMyTemplate()); + + if (WeaponTemplate != none) + { + KnockbackDamage = WeaponTemplate.fKnockbackDamageAmount >= 0.0f ? WeaponTemplate.fKnockbackDamageAmount : DefaultDamage; + KnockbackRadius = WeaponTemplate.fKnockbackDamageRadius >= 0.0f ? WeaponTemplate.fKnockbackDamageRadius : DefaultRadius; + } + else + { + KnockbackDamage = DefaultDamage; + KnockbackRadius = DefaultRadius; + } + + kNewTargetState = NewGameState.GetGameStateForObjectID(CurrentTarget.ObjectID); + TargetUnit = XComGameState_Unit(kNewTargetState); + if(TargetUnit != none) //Only units can be knocked back + { + TilesEntered.Length = 0; + GetTilesEnteredArray(AbilityContext, kNewTargetState, TilesEntered, AttackDirection, KnockbackDamage, NewGameState); + + //Only process the code below if the target went somewhere + if(TilesEntered.Length > 0) + { + WorldData = `XWORLD; + + if(bKnockbackDestroysNonFragile) + { + for(Index = 0; Index < TilesEntered.Length; ++Index) + { + HitTile = TilesEntered[Index]; + HitTile.Z += 1; + + DamageEvent = XComGameState_EnvironmentDamage(NewGameState.CreateNewStateObject(class'XComGameState_EnvironmentDamage')); + DamageEvent.DEBUG_SourceCodeLocation = "UC: X2Effect_Knockback:ApplyEffectToWorld"; + DamageEvent.DamageAmount = KnockbackDamage; + DamageEvent.DamageTypeTemplateName = 'Melee'; + DamageEvent.HitLocation = WorldData.GetPositionFromTileCoordinates(HitTile); + DamageEvent.Momentum = AttackDirection; + DamageEvent.DamageDirection = AttackDirection; //Limit environmental damage to the attack direction( ie. spare floors ) + DamageEvent.PhysImpulse = 100; + DamageEvent.DamageRadius = KnockbackRadius; + DamageEvent.DamageCause = ApplyEffectParameters.SourceStateObjectRef; + DamageEvent.DamageSource = DamageEvent.DamageCause; + DamageEvent.bRadialDamage = false; + } + } + + NewTileLocation = TilesEntered[TilesEntered.Length - 1]; + TargetUnit.SetVisibilityLocation(NewTileLocation); + } + } + } + } +} + +simulated function int CalculateDamageAmount(const out EffectAppliedData ApplyEffectParameters, out int ArmorMitigation, out int NewShred) +{ + return 0; +} + +simulated function bool PlusOneDamage(int Chance) +{ + return false; +} + +simulated function bool IsExplosiveDamage() +{ + return false; +} + +simulated function AddX2ActionsForVisualization(XComGameState VisualizeGameState, out VisualizationActionMetadata ActionMetadata, name EffectApplyResult) +{ + local X2Action_Knockback KnockbackAction; + + if (EffectApplyResult == 'AA_Success') + { + if( ActionMetadata.StateObject_NewState.IsA('XComGameState_Unit') ) + { + KnockbackAction = X2Action_Knockback(class'X2Action_Knockback'.static.AddToVisualizationTree(ActionMetadata, VisualizeGameState.GetContext(), false, ActionMetadata.LastActionAdded)); + if( OverrideRagdollFinishTimerSec >= 0 ) + { + KnockbackAction.OverrideRagdollFinishTimerSec = OverrideRagdollFinishTimerSec; + } + } + else if (ActionMetadata.StateObject_NewState.IsA('XComGameState_EnvironmentDamage') || ActionMetadata.StateObject_NewState.IsA('XComGameState_Destructible')) + { + //This can be added by other effects, so check to see whether this track already has one of these + class'X2Action_ApplyWeaponDamageToTerrain'.static.AddToVisualizationTree(ActionMetadata, VisualizeGameState.GetContext());//auto-parent to damage initiating action + } + } +} + +simulated function AddX2ActionsForVisualization_Tick(XComGameState VisualizeGameState, out VisualizationActionMetadata ActionMetadata, const int TickIndex, XComGameState_Effect EffectState) +{ + +} + +defaultproperties +{ + IncrementalStepSize=8.0 + + Begin Object Class=X2Condition_UnitProperty Name=UnitPropertyCondition + ExcludeTurret = true + ExcludeDead = false + FailOnNonUnits = true + End Object + + TargetConditions.Add(UnitPropertyCondition) + + DamageTypes.Add("KnockbackDamage"); + + OverrideRagdollFinishTimerSec=-1 + + OnlyOnDeath=true + + ApplyChanceFn=WasTargetPreviouslyDead +} \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc new file mode 100644 index 000000000..afd85acd4 --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc @@ -0,0 +1,386 @@ +//--------------------------------------------------------------------------------------- +// FILE: XComGameStateContext_Falling.uc +// AUTHOR: Russell Aasland -- 8/8/2014 +// PURPOSE: This context is used with falling events that require their own game state +// object. +// +//--------------------------------------------------------------------------------------- +// Copyright (c) 2016 Firaxis Games, Inc. All rights reserved. +//--------------------------------------------------------------------------------------- + +class XComGameStateContext_Falling extends XComGameStateContext + native(Core) + config(GameCore); + +var() StateObjectReference FallingUnit; +var() TTile StartLocation; + +var() array LandingLocations; +var() array EndingLocations; + +var() array LandedUnits; + +var const config int MinimumFallingDamage; + +function bool Validate(optional EInterruptionStatus InInterruptionStatus) +{ + return true; +} + +function XComGameState ContextBuildGameState() +{ + local XComGameState NewGameState; + local XComGameState_Unit FallingUnitState, LandedUnitState; + local X2CharacterTemplate CharacterTemplate; + local XComGameState_EnvironmentDamage WorldDamage; + local XGUnit Unit; + local float FallingDamage; + local TTile Tile; + local TTile LastEndLocation; + local StateObjectReference LandedUnit; + local int x; + local int iStoryHeightInTiles; + local int iNumStoriesFallen; + local XComGameStateHistory History; + + History = `XCOMHISTORY; + + FallingUnitState = XComGameState_Unit(History.GetGameStateForObjectID(FallingUnit.ObjectID)); + CharacterTemplate = FallingUnitState.GetMyTemplate(); + + if(CharacterTemplate.bImmueToFalling) //Flying units are immune to falling + { + return none; + } + + SetDesiredVisualizationBlockIndex( History.GetEventChainStartIndex( ) ); + NewGameState = History.CreateNewGameState( true, self ); + + FallingUnitState = XComGameState_Unit( NewGameState.ModifyStateObject( class'XComGameState_Unit', FallingUnit.ObjectID ) ); + + if (CharacterTemplate.bIsTurret) // turrets don't fall + { + FallingDamage = FallingUnitState.GetCurrentStat( eStat_HP ); + FallingUnitState.TakeDamage( NewGameState, FallingDamage, 0, 0, , , , , , , , true ); + FallingUnitState.bFallingApplied = true; + FallingUnitState.RemoveStateFromPlay( ); + } + else + { + LastEndLocation = EndingLocations[EndingLocations.Length - 1]; + + FallingUnitState.SetVisibilityLocation(LastEndLocation); + + + // Damage calculation for falling, per designer instructions via Hansoft, + // "Falling DMG should be flat for XCOM: Should be 2 damage per floor." + // Note: Griffin says this rule should apply to all units, not just "for XCOM". + // mdomowicz 2015_08_06 + iStoryHeightInTiles = 4; + iNumStoriesFallen = (StartLocation.Z - LastEndLocation.Z) / iStoryHeightInTiles; + FallingDamage = 2 * iNumStoriesFallen; + + + if(FallingDamage < MinimumFallingDamage) + { + FallingDamage = MinimumFallingDamage; + } + + if(!FallingUnitState.IsImmuneToDamage('Falling') && ((StartLocation.Z - LastEndLocation.Z >= iStoryHeightInTiles) || (LandedUnits.Length > 0))) + { + FallingUnitState.TakeDamage(NewGameState, FallingDamage, 0, 0, , , , , , , , true); + } + + foreach LandedUnits(LandedUnit) + { + Unit = XGUnit(`XCOMHISTORY.GetVisualizer(LandedUnit.ObjectID)); + if(Unit != none) + { + LandedUnitState = XComGameState_Unit(NewGameState.ModifyStateObject(class'XComGameState_Unit', LandedUnit.ObjectID)); + + if (LandedUnitState.IsImmuneToDamage( 'Falling' )) + continue; + + LandedUnitState.TakeDamage(NewGameState, FallingDamage, 0, 0, , , , , , , , true); + } + } + + Tile = StartLocation; + for(x = 0; x < LandingLocations.Length; ++x) + { + while(Tile != LandingLocations[x]) + { + --Tile.Z; + + WorldDamage = XComGameState_EnvironmentDamage(NewGameState.CreateNewStateObject(class'XComGameState_EnvironmentDamage')); + + WorldDamage.DamageTypeTemplateName = 'Falling'; + WorldDamage.DamageCause = FallingUnitState.GetReference(); + WorldDamage.DamageSource = WorldDamage.DamageCause; + WorldDamage.bRadialDamage = false; + WorldDamage.HitLocationTile = Tile; + WorldDamage.DamageTiles.AddItem(WorldDamage.HitLocationTile); + WorldDamage.bAllowDestructionOfDamageCauseCover = true; + + WorldDamage.DamageDirection.X = 0.0f; + WorldDamage.DamageDirection.Y = 0.0f; + WorldDamage.DamageDirection.Z = (Tile.Z == LandingLocations[x].Z) ? 1.0f : -1.0f; // change direction of landing tile so as to not destroy the floor they should be landing on + + WorldDamage.DamageAmount = (FallingUnitState.UnitSize == 1) ? 10 : 100; // Large Units smash through more things + WorldDamage.bAffectFragileOnly = false; + } + + Tile = EndingLocations[x]; + } + + `XEVENTMGR.TriggerEvent('UnitMoveFinished', FallingUnitState, FallingUnitState, NewGameState); + } + + return NewGameState; +} + +protected function ContextBuildVisualization() +{ + local VisualizationActionMetadata ActionMetadata; + local VisualizationActionMetadata EmptyTrack; + local XComGameState_EnvironmentDamage DamageEventStateObject; + local XGUnit Unit; + local TTile LastEndLocation; + local StateObjectReference LandedUnit; + local X2VisualizerInterface TargetVisualizerInterface; + local XComGameState_Unit FallingUnitState; + local XComGameState_Effect TestEffect; + local bool PartOfCarry; + local X2Action_MoveTeleport MoveTeleport; + local vector PathEndPos; + local PathingInputData PathingData; + local PathingResultData ResultData; + local X2Action_UpdateFOW FOWUpdate; + local X2Action FallingAction; + + ActionMetadata = EmptyTrack; + ActionMetadata.VisualizeActor = `XCOMHISTORY.GetVisualizer(FallingUnit.ObjectID); + `XCOMHISTORY.GetCurrentAndPreviousGameStatesForObjectID(FallingUnit.ObjectID, ActionMetadata.StateObject_OldState, ActionMetadata.StateObject_NewState, eReturnType_Reference, AssociatedState.HistoryIndex); + + class'X2Action_WaitForWorldDamage'.static.AddToVisualizationTree( ActionMetadata, self ); + + if (XComGameState_Unit(ActionMetadata.StateObject_OldState).GetMyTemplate().bIsTurret) + { + class'X2Action_RemoveUnit'.static.AddToVisualizationTree( ActionMetadata, self ); + + class'X2Action_ApplyWeaponDamageToUnit'.static.AddToVisualizationTree( ActionMetadata, self ); + + return; + } + + PartOfCarry = false; + FallingUnitState = XComGameState_Unit(`XCOMHISTORY.GetGameStateForObjectID(FallingUnit.ObjectID)); + if( FallingUnitState != None ) + { + TestEffect = FallingUnitState.GetUnitAffectedByEffectState(class'X2AbilityTemplateManager'.default.BeingCarriedEffectName); + if( TestEffect != None ) + { + PartOfCarry = true; + } + + TestEffect = FallingUnitState.GetUnitAffectedByEffectState(class'X2Ability_CarryUnit'.default.CarryUnitEffectName); + if( TestEffect != None ) + { + PartOfCarry = true; + } + } + + FOWUpdate = X2Action_UpdateFOW( class'X2Action_UpdateFOW'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded ) ); + FOWUpdate.BeginUpdate = true; + + if (XGUnit(ActionMetadata.VisualizeActor).GetPawn().RagdollFlag == ERagdoll_Never) + { + PathEndPos = `XWORLD.GetPositionFromTileCoordinates( XComGameState_Unit(ActionMetadata.StateObject_NewState).TileLocation ); + MoveTeleport = X2Action_MoveTeleport( class'X2Action_MoveTeleport'.static.AddToVisualizationTree( ActionMetadata, self ) ); + MoveTeleport.ParsePathSetParameters( 0, PathEndPos, 0, PathingData, ResultData ); + MoveTeleport.SnapToGround = true; + FallingAction = MoveTeleport; + } + else if( PartOfCarry ) + { + FallingAction = class'X2Action_UnitFallingNoRagdoll'.static.AddToVisualizationTree(ActionMetadata, self); + } + else + { + FallingAction = class'X2Action_UnitFalling'.static.AddToVisualizationTree(ActionMetadata, self); + } + + FOWUpdate = X2Action_UpdateFOW( class'X2Action_UpdateFOW'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded) ); + FOWUpdate.EndUpdate = true; + + LastEndLocation = EndingLocations[ EndingLocations.Length - 1 ]; + if (StartLocation.Z - LastEndLocation.Z >= 4) + { + class'X2Action_ApplyWeaponDamageToUnit'.static.AddToVisualizationTree(ActionMetadata, self); + } + + //Allow the visualizer to do any custom processing based on the new game state. For example, units will create a death action when they reach 0 HP. + TargetVisualizerInterface = X2VisualizerInterface(ActionMetadata.VisualizeActor); + if (TargetVisualizerInterface != none) + TargetVisualizerInterface.BuildAbilityEffectsVisualization(AssociatedState, ActionMetadata); + + foreach LandedUnits( LandedUnit ) + { + Unit = XGUnit(`XCOMHISTORY.GetVisualizer(LandedUnit.ObjectID)); + if (Unit != none) + { + ActionMetadata = EmptyTrack; + ActionMetadata.VisualizeActor = `XCOMHISTORY.GetVisualizer( LandedUnit.ObjectID ); + `XCOMHISTORY.GetCurrentAndPreviousGameStatesForObjectID(LandedUnit.ObjectID, ActionMetadata.StateObject_OldState, ActionMetadata.StateObject_NewState, eReturnType_Reference, AssociatedState.HistoryIndex); + + class'X2Action_ApplyWeaponDamageToUnit'.static.AddToVisualizationTree(ActionMetadata, self); + + TargetVisualizerInterface = X2VisualizerInterface(ActionMetadata.VisualizeActor); + if (TargetVisualizerInterface != none) + TargetVisualizerInterface.BuildAbilityEffectsVisualization(AssociatedState, ActionMetadata); + + + } + } + + // add visualization of environment damage, should be triggered by the fall + foreach AssociatedState.IterateByClassType( class'XComGameState_EnvironmentDamage', DamageEventStateObject ) + { + ActionMetadata = EmptyTrack; + ActionMetadata.StateObject_OldState = DamageEventStateObject; + ActionMetadata.StateObject_NewState = DamageEventStateObject; + ActionMetadata.VisualizeActor = `XCOMHISTORY.GetVisualizer(DamageEventStateObject.ObjectID); + class'X2Action_WaitForAbilityEffect'.static.AddToVisualizationTree(ActionMetadata, self, false, FallingAction); + class'X2Action_ApplyWeaponDamageToTerrain'.static.AddToVisualizationTree( ActionMetadata, self, false, FallingAction); + } +} + +/// +/// Falling contexts are the result of the falling game mechanic which is trigged by the destruction of a floor beneath a unit. As a result we perform custom +/// merge logic that will take the falling visualization and attach it to the action where the floor was destroyed +/// +function MergeIntoVisualizationTree(X2Action BuildTree, out X2Action VisualizationTree) +{ + local XComGameStateHistory History; + local XComGameStateVisualizationMgr VisualizationMgr; + local XComGameStateContext Context; + local array EnvironmentalDestructionActions; + local array UnitDeathActions; + local X2Action_ApplyWeaponDamageToTerrain EvaluateAction; + local X2Action_Death EvaluateActionDeath; + local X2Action AttachToAction; + local XComGameState_EnvironmentDamage DamageStateObject; + local X2Action DamageCauseAction; + local array TreeEndNodes; + local array Nodes; + local int Index; + local int TileIndex; + local int FoundIndex; + local bool bFirstFallInChain; + local TTile BelowStartLocation; + + History = `XCOMHISTORY; + VisualizationMgr = `XCOMVISUALIZATIONMGR; + VisualizationMgr.GetNodesOfType(VisualizationTree, class'X2Action_ApplyWeaponDamageToTerrain', EnvironmentalDestructionActions); + + BelowStartLocation = StartLocation; + BelowStartLocation.Z = BelowStartLocation.Z - 1; + + //Locate the environmental destruction action that corresponds to the fall + FoundIndex = -1; + for (Index = 0; Index < EnvironmentalDestructionActions.Length; ++Index) + { + EvaluateAction = X2Action_ApplyWeaponDamageToTerrain(EnvironmentalDestructionActions[Index]); + + //If we have not found a suitable match by tile, fall back to a match using the parent game state information + if ((EvaluateAction.StateChangeContext.AssociatedState.ParentGameState == AssociatedState.ParentGameState || + EvaluateAction.StateChangeContext.AssociatedState == AssociatedState.ParentGameState) && FoundIndex < 0 ) + { + //Default to attaching to the first action. For additional actions, see if they are a better match + AttachToAction = EvaluateAction; + FoundIndex = Index; + } + else + { + //Look for a perfect match + DamageStateObject = XComGameState_EnvironmentDamage(EvaluateAction.Metadata.StateObject_NewState); + for (TileIndex = 0; TileIndex < DamageStateObject.DamageTiles.Length; ++TileIndex) + { + if (DamageStateObject.DamageTiles[TileIndex] == StartLocation || + DamageStateObject.DamageTiles[TileIndex] == BelowStartLocation) + { + AttachToAction = EvaluateAction; + FoundIndex = Index; + break; + } + } + } + } + + //Look for area damage visualization, which requires that we search for any death actions that will be run by the falling unit. + if (VisualizationTree.HasChildOfType(class'X2Action_WaitForDestructibleActorActionTrigger') || + VisualizationTree.HasChildOfType(class'X2Action_WaitForWorldDamage')) + { + //Also look for death actions. If we find one, then using it is prioritized over area damage + VisualizationMgr.GetNodesOfType(VisualizationTree, class'X2Action_Death', UnitDeathActions); + for (Index = 0; Index < UnitDeathActions.Length; ++Index) + { + EvaluateActionDeath = X2Action_Death(UnitDeathActions[Index]); + if (EvaluateActionDeath.Metadata.StateObject_NewState.ObjectID == FallingUnit.ObjectID) + { + AttachToAction = EvaluateActionDeath.ParentActions[0]; //Attach to one of the parents of the death action + break; + } + } + } + + if (FoundIndex > -1) + { + //Ascertain whether we are the first fall in a chain of falls or not. The first in the chain will be inserted, the rest will be attached as leaf sub trees that visualize + //simultaneously with the first fall + for (Index = EventChainStartIndex; Context == none || !Context.bLastEventInChain; ++Index) + { + Context = History.GetGameStateFromHistory(Index).GetContext(); + if (XComGameStateContext_Falling(Context) != none) + { + bFirstFallInChain = Context == self; + break; + } + } + + //Find the X2Action_MarkerTreeInsertEnd within the BuildTree we are trying to merge + VisualizationMgr.GetNodesOfType(BuildTree, class'X2Action_MarkerTreeInsertEnd', TreeEndNodes); + + if (bFirstFallInChain && TreeEndNodes.Length == 1) + { + //Insert the sub tree for falling, forces it into the critical path of actions + VisualizationMgr.InsertSubtree(BuildTree, TreeEndNodes[0], AttachToAction); + } + else + { + // Attach the fall sub tree as a leaf tree + VisualizationMgr.ConnectAction(BuildTree, VisualizationTree, false, AttachToAction); + } + + //Get the action that is leading to the apply weapon damage to terrain. See if the unit falling here also caused the damage leading to the fall. + DamageCauseAction = EvaluateAction.ParentActions[0]; + if (DamageCauseAction.Metadata.StateObject_NewState.ObjectID == FallingUnit.ObjectID) + { + //Since they caused the damage, see if they have an enter cover action that we need to cancel + VisualizationMgr.GetNodesOfType(VisualizationTree, class'X2Action_EnterCover', Nodes); + for (Index = 0; Index < Nodes.Length; ++Index) + { + //Only cancel their exit cover actions that occurred during or after the damage + if (Nodes[Index].StateChangeContext.AssociatedState.HistoryIndex >= DamageCauseAction.StateChangeContext.AssociatedState.HistoryIndex) + { + VisualizationMgr.DestroyAction(Nodes[Index]); + } + } + } + } +} + +function string SummaryString() +{ + return "XComGameStateContext_Falling"; +} \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/X2WOTCCommunityHighlander.x2proj b/X2WOTCCommunityHighlander/X2WOTCCommunityHighlander.x2proj index 575029d1d..4463316a6 100644 --- a/X2WOTCCommunityHighlander/X2WOTCCommunityHighlander.x2proj +++ b/X2WOTCCommunityHighlander/X2WOTCCommunityHighlander.x2proj @@ -640,6 +640,9 @@ Content + + Content + Content @@ -796,6 +799,9 @@ Content + + Content + Content From d19b3216c839a9283836eb1cf36a7601761aa60e Mon Sep 17 00:00:00 2001 From: furudee Date: Sun, 29 Dec 2024 17:03:15 +0200 Subject: [PATCH 2/2] Issue X2CommunityCore#1431 - Knockback trace to line trace, knocked back units can take fall damage and may fly over units, merging logic with visualization trees --- .../XComGame/Classes/X2Effect_Knockback.uc | 97 ++++++++++++++++++- .../Classes/XComGameStateContext_Falling.uc | 36 +++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc index 01345d41a..521b6b617 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_Knockback.uc @@ -35,6 +35,12 @@ var config float DefaultDamage; var config float DefaultRadius; var config array KnockbackDistanceOverrides; +// Variables for Issue #1431 +var XComGameState_Unit FallingUnit; +var TTile StartingTile; +var TTile LandingTile; +var TTile EndingTile; + function name WasTargetPreviouslyDead(const out EffectAppliedData ApplyEffectParameters, XComGameState_BaseObject kNewTargetState, XComGameState NewGameState) { // A unit that was dead before this game state should not get a knockback, they are already a corpse @@ -187,8 +193,10 @@ private function GetTilesEnteredArray(XComGameStateContext_Ability AbilityContex Extents.Z = TargetUnitPawn.CylinderComponent.CollisionHeight; } } - - if( WorldData.GetAllActorsTrace(StartLocation, KnockbackToLocation, Hits, Extents) ) + /// HL-Docs: ref:Bugfixes; issue:1431 + /// Knockback actor trace no longer collides with ground, which often resulted in units not getting knocked back at all + // Single line for Issue #1431 - comment out Extents to make it a line trace + if( WorldData.GetAllActorsTrace(StartLocation, KnockbackToLocation, Hits /*, Extents */) ) { foreach Hits(TraceHitInfo) { @@ -227,7 +235,16 @@ private function GetTilesEnteredArray(XComGameStateContext_Ability AbilityContex { TileUnits = WorldData.GetUnitsOnTile(TempTile); if (TileUnits.Length > 0) - break; + { + /// HL-Docs: ref:Bugfixes; issue:1431 + /// Units knocked back may fly over units and land on them if they are high enough to take fall damage + // Start Issue #1431 + if ((StartTile.Z - TempTile.Z) < 4) + { + break; + } + // End Issue #1431 + } } if (LastTempTile != TempTile) @@ -242,10 +259,84 @@ private function GetTilesEnteredArray(XComGameStateContext_Ability AbilityContex //Move the target unit to the knockback location if (OutTilesEntered.Length == 0 || OutTilesEntered[OutTilesEntered.Length - 1] != LastTempTile) OutTilesEntered.AddItem(LastTempTile); + + /// HL-Docs: ref:Bugfixes; issue:1431 + /// Units knocked back can now take fall damage + // Start Issue #1431 + if ((StartTile.Z - OutTilesEntered[OutTilesEntered.Length - 1].Z) >= 4) + { + FallingUnit = XComGameState_Unit(kNewTargetState); + StartingTile = StartTile; + LandingTile = OutTilesEntered[OutTilesEntered.Length - 1]; + + TileUnits = WorldData.GetUnitsOnTile(LandingTile); + // if the unit will fall on someone + if (TileUnits.Length > 0) + { + // find a different ending point from the tile we're landing on + TestLocation = WorldData.FindClosestValidLocation(WorldData.GetPositionFromTileCoordinates(LandingTile), false, false, false); + EndingTile = WorldData.GetTileCoordinatesFromPosition(TestLocation); + + // replace the ending tile with an unblocked tile or unit will end up inside unit they're landing on + OutTilesEntered[OutTilesEntered.Length - 1] = EndingTile; + } + + // register a delegate to submit a new falling gamestate after the current one + History.RegisterOnNewGameStateDelegate(DelayedFalling); + } + // End Issue #1431 } } } +// Start Issue #1431 +private function DelayedFalling(XComGameState PreviousGameState) +{ + local XComGameStateHistory History; + local XComGameState NewGameState; + local XComGameStateContext_Falling FallingContext; + local TTile AboveTile; + local array TileUnits; + + History = `XCOMHISTORY; + + // unregister now or suffer infinite loop + History.UnRegisterOnNewGameStateDelegate(DelayedFalling); + + FallingContext = XComGameStateContext_Falling(class'XComGameStateContext_Falling'.static.CreateXComGameStateContext()); + + // make the start location directly above the landing point + AboveTile.X = LandingTile.X; + AboveTile.Y = LandingTile.Y; + AboveTile.Z = StartingTile.Z; + + FallingContext.FallingUnit = FallingUnit.GetReference(); + FallingContext.StartLocation = AboveTile; + FallingContext.LandingLocations.AddItem(LandingTile); + + // get all units on the tile unit is landing on + TileUnits = `XWORLD.GetUnitsOnTile(LandingTile); + + // don't want unit to fall on itself + TileUnits.RemoveItem(FallingUnit.GetReference()); + + // will fall on someone else + if (TileUnits.Length > 0) + { + FallingContext.LandedUnits = TileUnits; + FallingContext.EndingLocations.AddItem(EndingTile); + } + else + { + FallingContext.EndingLocations.AddItem(LandingTile); + } + + NewGameState = FallingContext.ContextBuildGameState(); + + `TACTICALRULES.SubmitGameState(NewGameState); +} +// End Issue #1431 + simulated function ApplyEffectToWorld(const out EffectAppliedData ApplyEffectParameters, XComGameState NewGameState) { local XComGameStateContext_Ability AbilityContext; diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc index afd85acd4..f5ae41665 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameStateContext_Falling.uc @@ -279,6 +279,10 @@ function MergeIntoVisualizationTree(X2Action BuildTree, out X2Action Visualizati local bool bFirstFallInChain; local TTile BelowStartLocation; + // Variables for Issue #1431 + local X2Action UnwantedAction; + local X2Action ChildAction; + History = `XCOMHISTORY; VisualizationMgr = `XCOMVISUALIZATIONMGR; VisualizationMgr.GetNodesOfType(VisualizationTree, class'X2Action_ApplyWeaponDamageToTerrain', EnvironmentalDestructionActions); @@ -378,6 +382,38 @@ function MergeIntoVisualizationTree(X2Action BuildTree, out X2Action Visualizati } } } + /// HL-Docs: ref:Bugfixes; issue:1431 + /// Visualization tree merging logic when a fall is triggered by knockback + // Start Issue #1431 + else + { + // Ascertain we're coming from a knockback triggering a fall + VisualizationMgr.GetNodesOfType(VisualizationTree, class'X2Action_Knockback', Nodes); + + if (Nodes.Length == 0) + { + return; + } + + // remove the actions we don't want, reparent the children + UnwantedAction = VisualizationMgr.GetNodeOfType(BuildTree, class'X2Action_UnitFalling'); + + if (UnwantedAction != None) + { + for (Index = UnwantedAction.ChildActions.Length - 1; Index >= 0; Index--) + { + ChildAction = UnwantedAction.ChildActions[Index]; + VisualizationMgr.DisconnectAction(ChildAction); + VisualizationMgr.ConnectAction(ChildAction, BuildTree, false, , UnwantedAction.ParentActions); + } + + VisualizationMgr.DisconnectAction(UnwantedAction); + } + + // use parent class to merge the visualization trees + super.MergeIntoVisualizationTree(BuildTree, VisualizationTree); + } + // End Issue #1431 } function string SummaryString()