505 lines
13 KiB
Ucode
505 lines
13 KiB
Ucode
//=============================================================================
|
|
// 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<ImpactInfo> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |