//============================================================================= // KFProjectileStickHelper //============================================================================= // Manages projectile sticking and pinning functionality //============================================================================= // Killing Floor 2 // Copyright (C) 2019 Tripwire Interactive LLC //============================================================================= class KFProjectileStickHelper extends Object within KFProjectile; var transient Pawn PinPawn; var transient name PinBoneName; var transient RB_ConstraintActorSpawnable PinConstraint; var transient vector PinLocation; var transient Actor PinHitActor; var AkEvent StickAkEvent; /** Tries to stick projectile to hit actor. alternatively, tries to bounce it off of hit actor. */ simulated function TryStick(vector HitNormal, optional vector HitLocation, optional Actor HitActor) { local TraceHitInfo HitInfo; if (Instigator == None || !Instigator.IsLocallyControlled() || (Physics == PHYS_None && StuckToActor != none)) { return; } if (HitActor != none && (HitActor == StuckToActor || HitActor == PinPawn)) { return; } GetImpactInfo(Velocity, HitLocation, HitNormal, HitInfo); if (HitInfo.HitComponent != none && GetImpactResult(HitActor, HitInfo.HitComponent)) { Stick(HitActor, HitLocation, HitNormal, HitInfo); } } /** Get all relevant impact info (called after collision occurs to fill in details that we don't get in HitWall or ProcessTouch) */ simulated function GetImpactInfo(vector in_Velocity, out vector out_HitLocation, out vector out_HitNormal, out TraceHitInfo out_HitInfo) { local vector VelNorm; local vector VelScaled; VelNorm = Normal(in_Velocity); VelScaled = VelNorm * 30; Trace(out_HitLocation, out_HitNormal, out_HitLocation + VelScaled, out_HitLocation - VelScaled,,, out_HitInfo, TRACEFLAG_Bullet /*for complex collision*/ ); } /** Returns appropriate interaction with HitActor (stick or ignore, for now. add bounce later?) */ simulated function bool GetImpactResult(Actor HitActor, PrimitiveComponent HitComp) { local KFPawn_Human KFP; local KFDestructibleActor D; local StaticMeshComponent StaticMeshComp; if (HitActor == none) { return true; } if (HitActor.RemoteRole == ROLE_None && !HitActor.bWorldGeometry) { return false; } // if we've already been dislodged from an actor, don't keep trying to stick to it while falling if (HitActor.bTearOff || HitActor.bDeleteMe || HitActor.bPendingDelete || HitActor == PrevStuckToActor) { return false; } StaticMeshComp = StaticMeshComponent(HitComp); if (StaticMeshComp != none) { // NOTE: Door actors fall into this category! // pass through meshes that can move return !StaticMeshComp.CanBecomeDynamic(); } KFP = KFPawn_Human(HitActor); if (KFP != none) { // bounce off of player pawns, stick to other pawns return false; } D = KFDestructibleActor(HitActor); if (D != none) { // don't react to client-side-only destructibles, stick to others return D.ReplicationMode != RT_ClientSide; } return true; } /** Stops movement of projectile and calculates orientation to surface */ simulated function Stick(Actor HitActor, vector HitLocation, vector HitNormal, const out TraceHitInfo HitInfo) { local int BoneIdx; local KFPawn_Monster HitMonster; local array HitZoneImpactList; local vector StartTrace, EndTrace, Direction, ClosestBoneLocation; local name BoneName; BoneName = HitInfo.BoneName; HitMonster = KFPawn_Monster(HitActor); if (HitMonster != none) { // get injury hit zone StartTrace = HitLocation; Direction = Normal(Velocity); EndTrace = StartTrace + Direction * (HitMonster.CylinderComponent.CollisionRadius * 6.0); TraceProjHitZones(HitMonster, EndTrace, StartTrace, HitZoneImpactList); if (BoneName == '') { // get the best bone to attach to ClosestBoneLocation = HitMonster.Mesh.GetClosestCollidingBoneLocation(HitLocation, true, false); BoneName = HitMonster.Mesh.FindClosestBone(ClosestBoneLocation, ClosestBoneLocation); } // do impact damage if (KFWeapon(Owner) != none) { HitZoneImpactList[0].RayDir = Normal(EndTrace - StartTrace); // add a raydir here since TraceProjHitZones doesn't fill this out (so certain afflictions apply) KFWeapon(Owner).HandleProjectileImpact(WeaponFireMode, HitZoneImpactList[0], PenetrationPower); } } if (!IsZero(HitLocation)) { SetLocation(HitLocation); } SetStickOrientation(HitNormal); BoneIdx = INDEX_NONE; if (BoneName != '') { BoneIdx = GetBoneIndexFromActor(HitActor, BoneName); } StickToActor(HitActor, HitInfo.HitComponent, BoneIdx, true); if (Role < ROLE_Authority) { Outer.ServerStick(HitActor, BoneIdx, StuckToLocation, StuckToRotation); } if (WorldInfo.NetMode != NM_DedicatedServer && StickAkEvent != none) { PlaySoundBase(StickAkEvent); } } /** Changes the base of the charge to the stick actor and sets its relative loc/rot */ simulated function StickToActor(Actor StickTo, PrimitiveComponent HitComp, int BoneIdx, optional bool bCalculateRelativeLocRot) { local SkeletalMeshComponent SkelMeshComp; local Name BoneName; local vector RelStuckToLocation; local rotator RelStuckToRotation; local KFPawn StickToPawn; StickToPawn = KFPawn(StickTo); if (bCanPin && (StickToPawn == none || StickToPawn.bCanBePinned)) { // if StickTo pawn is dead, pin it and keep flying if (Role == ROLE_Authority) { if (StickToPawn != none && !StickToPawn.IsAliveAndWell()) { if (PinPawn == none) { Pin(StickTo, BoneIdx); } return; } } if (WorldInfo.NetMode != NM_DedicatedServer && PinPawn != none) { if (StickToPawn == none) { // Pin pinned pawn to StickTo actor //PinPawn.Mesh.RetardRBLinearVelocity(vector(Rotation), 0.75); PinPawn.Mesh.SetRBPosition(Location, PinBoneName); PinConstraint = Spawn(class'RB_ConstraintActorSpawnable',,,Location); PinConstraint.InitConstraint(PinPawn, none, PinBoneName, ''); } PinPawn = none; } } else if (StickToPawn != none && !StickToPawn.IsAliveAndWell()) { return; } SetPhysics(PHYS_None); PrevStuckToActor = StuckToActor; StuckToActor = StickTo; StuckToBoneIdx = BoneIdx; // if we found a skel mesh, set our base to it and set relative loc/rot if (BoneIdx != INDEX_NONE) { SkelMeshComp = SkeletalMeshComponent(HitComp); BoneName = SkelMeshComp.GetBoneName(BoneIdx); if (bCalculateRelativeLocRot) { StuckToLocation = Location; StuckToRotation = Rotation; } SkelMeshComp.TransformToBoneSpace(BoneName, StuckToLocation, StuckToRotation, RelStuckToLocation, RelStuckToRotation); SetBase(StickTo,, SkelMeshComp, BoneName); SetRelativeLocation(RelStuckToLocation); SetRelativeRotation(RelStuckToRotation); } // otherwise, just set our base else { if (bCalculateRelativeLocRot) { // set replicated loc/rot StuckToLocation = Location; StuckToRotation = Rotation; } else { // set loc/rot to replicated loc/rot SetLocation(StuckToLocation); SetRotation(StuckToRotation); } SetBase(StickTo); } } simulated function Pin(Actor PinTo, int BoneIdx) { if (Role == ROLE_Authority) { bUpdateSimulatedPosition = false; PinActor = PinTo; PinBoneIdx = BoneIdx; } PinPawn = Pawn(PinTo); PinBoneName = PinPawn.Mesh.GetBoneName(BoneIdx); StuckToActor = none; StuckToBoneIdx = INDEX_None; SetBase(none); SetPhysics(PHYS_Falling); if (WorldInfo.NetMode != NM_Standalone) { SetLocation(StuckToLocation); SetRotation(StuckToRotation); } Velocity = Speed * vector(Rotation); } /** Attempts to retrieve skeletal mesh from actor */ simulated function SkeletalMeshComponent GetActorSkeletalMesh(Actor StickActor) { local Pawn P; local SkeletalMeshActor SM; P = Pawn(StickActor); if (P != none) { return P.Mesh; } SM = SkeletalMeshActor(StickActor); if (SM != none) { return SM.SkeletalMeshComponent; } return none; } /** Replicates stick to server from client */ function ServerStick(Actor StickTo, int BoneIdx, vector StickLoc, rotator StickRot) { bUpdateSimulatedPosition = true; StuckToLocation = StickLoc; StuckToRotation = StickRot; bForceNetUpdate = true; if (PinPawn != none) { PinPawn = none; } ReplicatedStick(StickTo, BoneIdx); } /** Calls "Stick" with replicated info */ simulated function ReplicatedStick(Actor StickTo, int BoneIdx) { StickToActor(StickTo, GetActorSkeletalMesh(StickTo), BoneIdx); } /** Gets index for passed-in bone name for different kinds of actors that have differently-named skeletalmeshcomponents */ simulated function int GetBoneIndexFromActor(Actor HitActor, Name BoneName) { local Pawn P; local SkeletalMeshActor SM; P = Pawn(HitActor); if (P != none) { return P.Mesh.MatchRefBone(BoneName); } SM = SkeletalMeshActor(HitActor); if (SM != none) { return SM.SkeletalMeshComponent.MatchRefBone(BoneName); } return INDEX_NONE; } /** Resets physics/collision vars to defaults */ simulated function UnStick() { PrevStuckToActor = StuckToActor; StuckToActor = none; StuckToBoneIdx = INDEX_NONE; StuckToLocation = vect(0,0,0); StuckToRotation = rot(0,0,0); SetBase(none); SetPhysics(default.Physics); } simulated function UnPin() { if (PinConstraint != none) { PinConstraint.TermConstraint(); } PinConstraint = none; PinPawn = none; } simulated function Tick(float DeltaTime) { local int i; local Pawn P; local KFFracturedMeshActor FracMesh; local KFDoorActor Door; local KFDestructibleActor Destructible; local Actor StuckTo; local vector HitLocation, HitNormal; local TraceHitInfo HitInfo; local Actor HitActor; local float PinRad; local vector VFront; local float StickPct; // @todo jdr: REMOVE THESE WHEN WE HAVE A BETTER SYSTEM local float PinRadMagicNumber, StickPctMagicNumber; if (PinPawn != none) { // @todo jdr: REMOVE MAGIC NUMBERS FROM COMMON CODE // Ideally, this code retrieves values from the projectile, or we could add tuneables to // the helper than can be set in each instance of the helper VFront = Normal(Velocity); StickPct = 1.0; PinRadMagicNumber = 3.0; StickPctMagicNumber = 0.3; if (IsZero(PinLocation)) { PinRad = PinPawn.CylinderComponent.CollisionRadius * PinRadMagicNumber; HitActor = Trace(HitLocation, HitNormal, Location, Location + VFront * PinRad,,, HitInfo, TRACEFLAG_Bullet); if (HitActor != none && HitActor != PinActor && Pawn(HitActor) == none && GetImpactResult(HitActor, HitInfo.HitComponent)) { GravityScale = 0.0; PinLocation = HitLocation; PinHitActor = HitActor; } } if (!IsZero(PinLocation)) { PinRad = PinPawn.CylinderComponent.CollisionRadius * PinRadMagicNumber; StickPct = VSize(PinLocation - Location) / PinRad; Velocity = VFront * MaxSpeed * StickPct * StickPct; } if (WorldInfo.NetMode != NM_DedicatedServer) { PinPawn.Mesh.SetRBLinearVelocity(Velocity); PinPawn.Mesh.SetRBPosition(Location, PinBoneName); if (Instigator != none && Instigator.IsLocallyControlled() && StickPct < StickPctMagicNumber) { Stick(PinHitActor, Location, HitNormal, HitInfo); } } } StuckTo = StuckToActor; if (StuckTo != none) { // always restart movement if torn off if (StuckTo.bTearOff && PinPawn == none) { UnStick(); return; } // if the bone we're stuck to is hidden (just head, probably), detatch P = Pawn(StuckTo); if (P != none) { if (P.Mesh.IsBoneHidden(StuckToBoneIdx)) { UnStick(); } return; } // if the non-pawn actor we're stuck to is going away (could be due to non-relevancy, in which case they will not be torn off), detatch if (StuckTo.bDeleteMe || StuckTo.bPendingDelete) { UnStick(); return; } // if the glass we're stuck to is fractured, detatch FracMesh = KFFracturedMeshActor(StuckTo); if (FracMesh != none && FracMesh.bHasLostChunk) { UnStick(); return; } // if the door we're stuck to is moving, detatch // (we can't set our base to doors because they're world geometry, so we won't follow when they move) Door = KFDoorActor(StuckTo); if (Door != none && (!Door.bDoorMoveCompleted || Door.bIsDestroyed)) { UnStick(); return; } if (LastTouchComponent != none) { // if the replicated destructible we're stuck to is destroyed, detatch Destructible = KFDestructibleActor(StuckTo); if (Destructible != none) { for (i = 0; i < Destructible.SubObjects.Length; ++i) { if (Destructible.SubObjects[i].Mesh == LastTouchComponent && Destructible.SubObjects[i].Health <= 0) { UnStick(); return; } } } } } }