//============================================================================= // KFProj_BallisticExplosive //============================================================================= // Superclass for projectiles that need to fly through the air quickly // and damage things they impact, but also explode //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - John "Ramm-Jaeger" Gibso //============================================================================= class KFProj_BallisticExplosive extends KFProjectile abstract hidedropdown; /** This projectile is a dud, it hit too soon */ var repnotify bool bDud; /** When true this projectile will collide/hit/explode when hitting teammates */ var() bool bCollideWithTeammates; var bool bClientDudHit; var repnotify bool bIsTimedExplosive; /** Distance this projectile arms itself at */ var() float ArmDistSquared; /** The effect to display when the projectile becomes a dud */ var(Projectile) ParticleSystem ProjDudTemplate; /** Information about grenade bounce effects for this class */ var KFImpactEffectInfo GrenadeBounceEffectInfo; /** Dampen amount for every bounce */ var() float DampenFactor; /** Dampen amount for parallel angle to velocity */ var() float DampenFactorParallel; /** Dampen amount for every bounce */ var() float WallHitDampenFactor; /** Dampen amount for parallel angle to velocity */ var() float WallHitDampenFactorParallel; /** A smaller speed will cause the projectile to stop */ var() float MinSpeedBeforeStop; /** Set to true to reset the projectile rotation when it stops because of low speed */ var() bool ResetRotationOnStop; /** How much to offset the emitter mesh when the grenade has landed so that it doesn't penetrate the ground */ var() vector LandedTranslationOffset; //to keep track if we are hitting an actor multiple times. var array ImpactList; replication { if ( Role == ROLE_Authority && !bNetOwner ) bDud; if( bNetInitial && !bNetOwner ) ArmDistSquared; if (bNetDirty) bIsTimedExplosive; } simulated function SyncOriginalLocation() { local KFPerk InstigatorPerk; local KFPawn KFP; KFP = KFPawn(Instigator); if( KFP != none ) { InstigatorPerk = KFP.GetPerk(); if( InstigatorPerk != none && InstigatorPerk.ShouldNeverDud() ) { ArmDistSquared = 0; } } // If server has already collided we need to recover our velocity. Reuse the code // in super by zeroing velocity. if ( Physics == PHYS_Falling && Role < ROLE_Authority && Instigator != none && Instigator.IsLocallyControlled() ) { Velocity = vect(0,0,0); } Super.SyncOriginalLocation(); } simulated event PreBeginPlay() { Super.PreBeginPlay(); AdjustCanDisintigrate(); } /** * This is only here to handle Projectile class calls * Should use TriggerExplosion instead which handles delayed shutdown */ simulated function Explode(vector HitLocation, vector HitNormal) { // Just do a standard explosion if its a locally authoritive player if( WorldInfo.NetMode == NM_Standalone || (WorldInfo.NetMode == NM_ListenServer && Instigator != none && Instigator.IsLocallyControlled()) ) { Super.Explode(HitLocation, HitNormal); return; } // Replication the explosion to the server if this is the owning client if( Owner != none && KFWeapon( Owner ) != none && Instigator != none ) { if( Instigator.Role < ROLE_Authority && Instigator.IsLocallyControlled() ) { KFWeapon(Owner).HandleClientProjectileExplosion(HitLocation, self); return; } } StopSimulating(); } /** Pre-process calling the Explode function so it can be overridden in subclasses */ simulated function CallExplode( vector HitLocation, vector HitNormal ) { // Move this projectile to the explode location since it might have been // replicated, and this ensures it happens in the right place SetLocation( HitLocation ); Super.Explode(HitLocation, HitNormal); } simulated event HitWall(vector HitNormal, actor Wall, PrimitiveComponent WallComp) { local Vector VNorm; local rotator NewRotation; local Vector Offset; local bool bWantsClientSideDudHit; local TraceHitInfo HitInfo; local float TraveledDistance; // Need to do client side dud hits if this is a client if( Instigator != none && Instigator.Role < ROLE_Authority ) { bWantsClientSideDudHit = true; } TraveledDistance = (`TimeSince(CreationTime) * Speed); TraveledDistance *= TraveledDistance; // Bounce off the wall and cause the shell to dud if we hit too close if( bDud || ((TraveledDistance < ArmDistSquared) || bIsTimedExplosive || (OriginalLocation == vect(0,0,0) && ArmDistSquared > 0))) { // Reflect off Wall w/damping VNorm = (Velocity dot HitNormal) * HitNormal; Velocity = -VNorm * WallHitDampenFactor + (Velocity - VNorm) * WallHitDampenFactorParallel; Speed = VSize(Velocity); if( (!bDud || ( bWantsClientSideDudHit && !bClientDudHit)) ) { SetIsDud(bWantsClientSideDudHit, HitNormal); } if ( WorldInfo.NetMode != NM_DedicatedServer && Pawn(Wall) == none ) { // do the impact effects `ImpactEffectManager.PlayImpactEffects(Location, Instigator, HitNormal, GrenadeBounceEffectInfo, true ); } // if we hit a pawn or we are moving too slowly stop moving and lay down flat if ( Speed < MinSpeedBeforeStop ) { ImpactedActor = Wall; SetPhysics(PHYS_None); if( ProjEffects != none ) { ProjEffects.SetTranslation(LandedTranslationOffset); } // Position the shell on the ground RotationRate.Yaw = 0; RotationRate.Pitch = 0; RotationRate.Roll = 0; NewRotation = Rotation; NewRotation.Pitch = 0; if(ResetRotationOnStop) { SetRotation(NewRotation); } Offset.Z = LandedTranslationOffset.X; SetLocation(Location + Offset); } if( !Wall.bStatic && Wall.bCanBeDamaged && (DamageRadius == 0 || bDamageDestructiblesOnTouch) && !CheckRepeatingTouch(Wall) ) { HitInfo.HitComponent = WallComp; HitInfo.Item = INDEX_None; Wall.TakeDamage( Damage, InstigatorController, Location, MomentumTransfer * Normal(Velocity), MyDamageType, HitInfo, self); } } if( !bDud && !bIsTimedExplosive ) { Super.HitWall(HitNormal, Wall, WallComp); } } /** returns terminal velocity (max speed while falling) for this actor. Unless overridden, it returns the TerminalVelocity of the PhysicsVolume in which this actor is located. * Overridden so we can have projectiles falling faster than the PhysicsVolume terminal velocity */ function float GetTerminalVelocity() { if( bDud ) { // Use the actor super class terminal velocity so duds fall slowly, but // the projectile can still move quickly when it is flying through the air return Super(Actor).GetTerminalVelocity(); } else { return Super.GetTerminalVelocity(); } } //============== // Touching simulated function ProcessTouch(Actor Other, Vector HitLocation, Vector HitNormal) { local bool bWantsClientSideDudHit; local float TraveledDistance; local Vector VNorm; // If we collided with a Siren shield, let the shield code handle touches if( Other.IsA('KFTrigger_SirenProjectileShield') ) { return; } if ( !bCollideWithTeammates && Pawn(Other) != None ) { // Don't hit teammates if( Other.GetTeamNum() == GetTeamNum() ) { return; } } // Need to do client side dud hits if this is a client if( Instigator != none && Instigator.Role < ROLE_Authority ) { bWantsClientSideDudHit = true; } TraveledDistance = (`TimeSince(CreationTime) * Speed); TraveledDistance *= TraveledDistance; if( (!bDud || ( bWantsClientSideDudHit && !bClientDudHit)) && ((TraveledDistance < ArmDistSquared) || bIsTimedExplosive || (OriginalLocation == vect(0,0,0) && ArmDistSquared > 0))) { // Don't touch the same actor multiple time's immediately after just // touching it if the TouchTimeThreshhold is set to greater than 0. // This was causing projectiles just to "stop" sometimes when hitting // dead/ragdolled pawns because it was counting as multiple penetrations if( LastTouched.Actor == Other && TouchTimeThreshhold > 0 && `TimeSince(LastTouched.Time) <= TouchTimeThreshhold ) { return; } //TODO: Add an impact sound here SetIsDud(bWantsClientSideDudHit, HitNormal); if (Other != Instigator && !Other.bStatic && Other.GetTeamNum() != GetTeamNum() && !CheckRepeatingTouch(Other)) { ProcessBulletTouch(Other, HitLocation, HitNormal); } // Reflect off Wall w/damping VNorm = (Velocity dot HitNormal) * HitNormal; Velocity = -VNorm * DampenFactor + (Velocity - VNorm) * DampenFactorParallel; Speed = VSize(Velocity); } if (!bDud && !bIsTimedExplosive) { // Process impact hits if (Other != Instigator && !Other.bStatic) { // check/ignore repeat touch events if( !CheckRepeatingTouch(Other) && Other.GetTeamNum() != GetTeamNum()) { ProcessBulletTouch(Other, HitLocation, HitNormal); } } if( WorldInfo.NetMode == NM_Standalone || (WorldInfo.NetMode == NM_ListenServer && Instigator != none && Instigator.IsLocallyControlled()) ) { Super.ProcessTouch( Other, HitLocation, HitNormal ); return; } if( Owner != none && KFWeapon( Owner ) != none && Instigator != none ) { if( Instigator.Role < ROLE_Authority && Instigator.IsLocallyControlled() ) { KFWeapon(Owner).HandleClientProjectileExplosion(HitLocation, self); Super.ProcessTouch( Other, HitLocation, HitNormal ); return; } } StopSimulating(); } } /** Handle bullet collision and damage */ simulated function ProcessBulletTouch(Actor Other, Vector HitLocation, Vector HitNormal) { if (ImpactList.Find(Other) != INDEX_NONE) { return; } if (Other != none) { ImpactList.AddItem(Other); } super.ProcessBulletTouch(Other, HitLocation, HitNormal); } simulated function SetIsDud(bool bWantsClientSideDudHit, vector HitNormal) { bDud = true; if (bWantsClientSideDudHit) { bClientDudHit = true; } LifeSpan = 1.0; SetPhysics(PHYS_Falling); GravityScale = 1.0; // Replace the flying effects with the dud mesh effects StopFlightEffects(); ProjFlightTemplate = ProjDudTemplate; ProjFlightTemplateZedTime = ProjDudTemplate; SpawnFlightEffects(); OnDudEffect(); } simulated function OnDudEffect(); /** Returns a list of hitzone impacts for a collision with a given pawn * @note: To trace the PhysicsAsset the pawn cylinder should have BlockZeroExtent=FALSE */ simulated function bool TraceProjHitZones(Pawn P, vector EndTrace, vector StartTrace, out array out_Hits) { local vector ClosestHit, Direction; Super.TraceProjHitZones(P, EndTrace, StartTrace, out_Hits); // If we didn't hit anything, that means we hit the big cylinder but // missed the body. Just grab the nearest bone and hit that if( bDud && out_Hits.length == 0 ) { P.Mesh.FindClosestBone(StartTrace, ClosestHit); if( !IsZero(ClosestHit) ) { StartTrace = ClosestHit; Direction = Normal(ClosestHit-StartTrace); EndTrace = StartTrace + Direction * (P.CylinderComponent.CollisionRadius * 6.0); Super.TraceProjHitZones(P, EndTrace, StartTrace, out_Hits); } } return out_Hits.Length > 0; } // for nukes && concussive force simulated protected function PrepareExplosionTemplate() { class'KFPerk_Demolitionist'.static.PrepareExplosive( Instigator, self ); super.PrepareExplosionTemplate(); } simulated protected function SetExplosionActorClass() { local KFPlayerReplicationInfo InstigatorPRI; if( bWasTimeDilated && Instigator != none ) { InstigatorPRI = KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo); if( InstigatorPRI != none ) { if( InstigatorPRI.bNukeActive && class'KFPerk_Demolitionist'.static.ProjectileShouldNuke( self ) ) { ExplosionActorClass = class'KFPerk_Demolitionist'.static.GetNukeExplosionActorClass(); } } } super.SetExplosionActorClass(); } defaultproperties { bBounce=true ProjEffectsFadeOutDuration=0.0 DampenFactor=0.025000 DampenFactorParallel=0.050000 WallHitDampenFactor=0.025000 WallHitDampenFactorParallel=0.050000 MinSpeedBeforeStop=40 ResetRotationOnStop=true LandedTranslationOffset=(X=2) bCollideComplex=TRUE // Ignore simple collision on StaticMeshes, and collide per poly // CollideActors=true allows detection via OverlappingActors or CollidingActors (for Siren scream) Begin Object Name=CollisionCylinder CollisionRadius=5.f CollisionHeight=5.f BlockNonZeroExtent=false CollideActors=true End Object // Lets the projectile collide with both the big collision cylinder, and any // parts of the pawn outside the collision cylinder ExtraLineCollisionOffsets.Add(()) bNetTemporary=False bBlockedByInstigator=false bAlwaysReplicateExplosion=true bUseClientSideHitDetection=true bNoReplicationToInstigator=false bReplicateLocationOnExplosion=true bValidateExplosionNormalOnClient=true bSyncToOriginalLocation=true bSyncToThirdPersonMuzzleLocation=true ExplosionActorClass=class'KFExplosionActor' TouchTimeThreshhold=0.15 Begin Object Class=AkComponent name=AmbientAkSoundComponent bStopWhenOwnerDestroyed=true bForceOcclusionUpdateInterval=true OcclusionUpdateInterval=0.25; End Object AmbientComponent=AmbientAkSoundComponent Components.Add(AmbientAkSoundComponent) bAutoStartAmbientSound=true bStopAmbientSoundOnExplode=true bCanDisintegrate=true GlassShatterType=FMGS_ShatterAll // network bAlwaysReplicateDisintegration=true AlwaysRelevantDistanceSquared=6250000 // 25m bCollideWithTeammates=false bDamageDestructiblesOnTouch=true }