//============================================================================= // KFProj_HRG_BallisticBouncer //============================================================================= // Projectile class for HRG Ballistic Bouncer //============================================================================= // Killing Floor 2 // Copyright (C) 2022 Tripwire Interactive LLC //============================================================================= class KFProj_HRG_BallisticBouncer extends KFProjectile; /** Dampen amount for every bounce */ var float DampenFactor; /** Dampen amount for parallel angle to velocity */ var float DampenFactorParallel; /** Sound mine makes when it hits something and bounces off */ var AkEvent BounceAkEvent; /** Sound mine makes when it hits something and comes to rest */ var AkEvent ImpactAkEvent; /** Sound mine makes when it hits something and bounces off */ var AkEvent BounceAkEventHeavy; /** Sound mine makes when it hits something and comes to rest */ var AkEvent ImpactAkEventHeavy; /** Particle System spawned when it hits something */ var ParticleSystem HitFXTemplate; var transient ParticleSystemComponent HitPSC; /** Particle System for the fade out burst **/ var ParticleSystem BurstFXTemplate; var transient ParticleSystemComponent BurstPSC; /** Sound for the fade out burst **/ var AkEvent BurstAkEvent; /** Decal settings */ var MaterialInterface ImpactDecalMaterial; var float ImpactDecalMinSize; var float ImpactDecalMaxSize; var float ImpactDecalThickness; var int MaxBounces; var int NumBounces; var float MaxInitialSpeedByCharge; var float MinInitialSpeedByCharge; var float MaxCollisionRadius; var float MinCollisionRadius; var float MaxCollisionHeight; var float MinCollisionHeight; var float MaxScalePerPercentage; var float MinScalePerPercentage; var int MaxBouncesPerPercentage; var int MinBouncesPerPercentage; var float MaxLifeSpanPerPercentage; var float MinLifeSpanPerPercentage; var float InheritedScale; var repnotify float fChargePercentage; var float fCachedCylinderWidth, fCachedCylinderHeight; var float ArmDistSquared; var bool bFadingOut; var float FadeOutTime; var transient byte RequiredImpactsForSeasonal; var transient array ImpactVictims; replication { if( bNetDirty ) InheritedScale, fChargePercentage, MaxBounces; } simulated event ReplicatedEvent( name VarName ) { if( VarName == nameOf(fChargePercentage)) { fCachedCylinderWidth = Lerp(MinCollisionRadius, MaxCollisionRadius, fChargePercentage); fCachedCylinderHeight = Lerp(MinCollisionHeight, MaxCollisionHeight, fChargePercentage); // CylinderComponent(CollisionComponent).SetCylinderSize(fCachedCylinderWidth, fCachedCylinderHeight); SetCollisionSize(fCachedCylinderWidth, fCachedCylinderHeight); ApplyVFXParams(fChargePercentage); } else { super.ReplicatedEvent( VarName ); } } simulated event PostBeginPlay() { Super.PostBeginPlay(); RequiredImpactsForSeasonal = class'KFSeasonalEventStats_Xmas2022'.static.GetMaxBallisticBouncerImpacts(); } simulated function SetInheritedScale(float Scale, float ChargePercentage) { local float NewSpeed; InheritedScale = Scale; fChargePercentage = ChargePercentage; fChargePercentage = FMax(0.1, fChargePercentage); if (WorldInfo.NetMode == NM_DedicatedServer) { SetCollisionSize(Lerp(MinCollisionRadius, MaxCollisionRadius, fChargePercentage), Lerp(MinCollisionHeight, MaxCollisionHeight, ChargePercentage)); } NewSpeed = Lerp(MinInitialSpeedByCharge, MaxInitialSpeedByCharge, fChargePercentage); Velocity = Normal(Velocity) * NewSpeed; Speed = NewSpeed; MaxBounces = Lerp(MinBouncesPerPercentage, MaxBouncesPerPercentage, fChargePercentage); LifeSpan = Lerp(MinLifeSpanPerPercentage, MaxLifeSpanPerPercentage, fChargePercentage); if (ProjEffects != none) { ProjEffects.SetScale(Lerp(MinScalePerPercentage, MaxScalePerPercentage, fChargePercentage)); } if (WorldInfo.NetMode != NM_DedicatedServer) { ApplyVFXParams(fChargePercentage); } bNetDirty=true; } function RestoreCylinder() { CylinderComponent(CollisionComponent).SetCylinderSize(fCachedCylinderWidth, fCachedCylinderHeight); } simulated function BounceNoCheckRepeatingTouch(vector HitNormal, Actor BouncedOff) { local vector VNorm; // Reflect off BouncedOff w/damping VNorm = (Velocity dot HitNormal) * HitNormal; if (NumBounces < MaxBounces) { Velocity = -VNorm + (Velocity - VNorm); } else { Velocity = -VNorm * DampenFactor + (Velocity - VNorm) * DampenFactorParallel; } Speed = VSize(Velocity); // Play a sound PlayImpactSound(); // Spawn impact particle system, server needs to send the message (it's the only one storing MaxBounces) if (WorldInfo.NetMode != NM_DedicatedServer) { if (NumBounces < MaxBounces) { PlayImpactVFX(HitNormal); } `ImpactEffectManager.PlayImpactEffects(Location, Instigator, VNorm, ImpactEffects); } ++NumBounces; } /** Adjusts movement/physics of projectile. * Returns true if projectile actually bounced / was allowed to bounce */ simulated function bool Bounce( vector HitNormal, Actor BouncedOff ) { local vector StartTrace, EndTrace, TraceLocation, TraceNormal; // Avoid crazy bouncing if (CheckRepeatingTouch(BouncedOff)) { CylinderComponent(CollisionComponent).SetCylinderSize(1,1); SetTimer(0.06f, false, nameof(RestoreCylinder)); return false; } BounceNoCheckRepeatingTouch(HitNormal, BouncedOff); if (WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.NetMode == NM_Standalone) { StartTrace = Location; EndTrace = Location - HitNormal * 200.f; Trace(TraceLocation, TraceNormal, EndTrace, StartTrace,,,,TRACEFLAG_Bullet); //DrawDebugLine(StartTrace, EndTrace, 0, 0, 255, true); SpawnImpactDecal(TraceLocation, HitNormal, fChargePercentage); } return true; } /** Plays a sound on impact */ simulated function PlayImpactSound( optional bool bIsAtRest ) { if( WorldInfo.NetMode != NM_DedicatedServer ) { if( bIsAtRest ) { if(fChargePercentage < 0.75f) PostAkEvent( ImpactAkEvent, true, true, false ); else PostAkEvent( ImpactAkEventHeavy, true, true, false ); } else { if(fChargePercentage < 0.75f) PostAkEvent( BounceAkEvent, true, true, false ); else PostAkEvent( BounceAkEventHeavy, true, true, false ); } } } simulated function PlayImpactVFX(vector HitNormal) { HitPSC = WorldInfo.MyEmitterPool.SpawnEmitter(HitFXTemplate, ProjEffects.GetPosition(), rotator(HitNormal)); HitPSC.SetFloatParameter(name("Charge"), fChargePercentage); } simulated function ProcessTouch(Actor Other, Vector HitLocation, Vector HitNormal) { local float TraveledDistance; TraveledDistance = (`TimeSince(CreationTime) * Speed); TraveledDistance *= TraveledDistance; // If we collided with a Siren shield, let the shield code handle touches if( Other.IsA('KFTrigger_SirenProjectileShield') ) { return; } if (Other != Instigator && Other.GetTeamNum() != GetTeamNum()) { // check/ignore repeat touch events if (!CheckRepeatingTouch(Other)) { ProcessBulletTouch(Other, HitLocation, HitNormal); } } } simulated function ProcessBulletTouch(Actor Other, Vector HitLocation, Vector HitNormal) { local Pawn Victim; local array HitZoneImpactList; local ImpactInfo ImpactInfoFallBack; local vector StartTrace, EndTrace, Direction, DirectionFallBack; local TraceHitInfo HitInfo; local KFWeapon KFW; Victim = Pawn( Other ); if ( Victim == none ) { if ( bDamageDestructiblesOnTouch && Other.bCanBeDamaged ) { HitInfo.HitComponent = LastTouchComponent; HitInfo.Item = INDEX_None; // force TraceComponent on fractured meshes Other.TakeDamage(Damage, InstigatorController, Location, MomentumTransfer * Normal(Velocity), MyDamageType, HitInfo, self); } // Reduce the penetration power to zero if we hit something other than a pawn or foliage actor if( InteractiveFoliageActor(Other) == None ) { PenetrationPower = 0; BounceNoCheckRepeatingTouch(HitNormal, Other); return; } } else { if (bSpawnShrapnel) { //spawn straight forward through the zed SpawnShrapnel(Other, HitLocation, HitNormal, rotator(Velocity), ShrapnelSpreadWidthZed, ShrapnelSpreadHeightZed); } StartTrace = HitLocation; Direction = Normal(Velocity); EndTrace = StartTrace + Direction * (Victim.CylinderComponent.CollisionRadius * 6.0); TraceProjHitZones(Victim, EndTrace, StartTrace, HitZoneImpactList); //`Log("HitZoneImpactList: " $HitZoneImpactList.Length); if (HitZoneImpactList.length == 0) { // This projectile needs this special case, the projectile bounces with everything constantly while changing cylinder size // Making it's direction kind of unpredictable when trying to find the hit zones // If we fail to find one with the default method, trace from Hit Location to Victim Location plus some distance that makes the trace end outside of the Victim // That will succeed in any case start - end points are inside or outside the collider DirectionFallBack = Normal(Victim.Location - StartTrace); EndTrace = Victim.Location + DirectionFallBack * (Victim.CylinderComponent.CollisionRadius * 6.0); //Victim.DrawDebugSphere(StartTrace, 12, 6, 255, 0, 0, true); //Victim.DrawDebugSphere(EndTrace, 12, 6, 0, 255, 0, true); //Victim.DrawDebugLine(StartTrace, EndTrace, 0, 0, 255, true); TraceProjHitZones(Victim, EndTrace, StartTrace, HitZoneImpactList); // For some reason with the bouncing hitting certain parts doesn't detect hit, we force a fallback if (HitZoneImpactList.length == 0) { //`Log("HitZoneImpactList: USING FALLBACK!"); ImpactInfoFallBack.HitActor = Victim; ImpactInfoFallBack.HitLocation = HitLocation; ImpactInfoFallBack.HitNormal = Direction; ImpactInfoFallBack.StartTrace = StartTrace; HitZoneImpactList.AddItem(ImpactInfoFallBack); } } if ( HitZoneImpactList.length > 0 ) { IncrementNumImpacts(Victim); HitZoneImpactList[0].RayDir = Direction; if( Owner != none ) { KFW = KFWeapon( Owner ); if( KFW != none ) { KFW.HandleProjectileImpact(WeaponFireMode, HitZoneImpactList[0], PenetrationPower); } } BounceNoCheckRepeatingTouch(HitNormal, Other); } } } simulated function IncrementNumImpacts(Pawn Victim) { local int i; local KFPlayerController KFPC; if (WorldInfo.NetMode != NM_Standalone) { if (WorldInfo.NetMode == NM_Client) { return; } } if (Victim == none) { return; } if (Victim.bCanBeDamaged == false || Victim.IsAliveAndWell() == false) { return; } KFPC = KFPlayerController(InstigatorController); if (KFPC == none) { return; } for (i = 0; i < ImpactVictims.Length; ++i) { if (Victim == ImpactVictims[i]) { return; } } ImpactVictims.AddItem(Victim); UpdateImpactsSeasonalObjective(KFPC); } function UpdateImpactsSeasonalObjective(KFPlayerController KFPC) { local byte ObjectiveIndex; ObjectiveIndex = 3; if (ImpactVictims.Length >= RequiredImpactsForSeasonal) { // Check parent controller. KFPC.ClientOnTryCompleteObjective(ObjectiveIndex, SEI_Winter); } } simulated event HitWall( vector HitNormal, Actor Wall, PrimitiveComponent WallComp ) { // Don't collide with other projectiles if( Wall.class == class ) { return; } Bounce( HitNormal, Wall ); } simulated function SpawnBurstFX() { local vector vec; if( WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.MyEmitterPool == none || ProjEffects == none ) { return; } BurstPSC = WorldInfo.MyEmitterPool.SpawnEmitter(BurstFXTemplate, ProjEffects.GetPosition(), rotator(vect(0,0,1)), self, , ,true); vec.X = fChargePercentage; vec.Y = fChargePercentage; vec.Z = fChargePercentage; BurstPSC.SetVectorParameter(name("BlobCharge"), vec); BurstPSC.SetFloatParameter(name("MineFxControlParam"), fChargePercentage); PostAkEvent(BurstAkEvent, true, true, false); } simulated function Tick(float Delta) { if (NumBounces < MaxBounces || bFadingOut) return; if (Speed < 20.0f) { bFadingOut = true; StopSimulating(); SpawnBurstFX(); // Tell clients to tear off and fade out on their own if( WorldInfo.NetMode != NM_Client ) { ClearTimer(nameof(Timer_Destroy)); SetTimer( 1.0f, false, nameOf(Timer_Destroy) ); } } } simulated function Timer_Destroy() { if (BurstPSC != none) { BurstPSC.DeactivateSystem(); } Destroy(); } simulated function ApplyVFXParams(float ChargePercent) { if (ProjEffects != none) { ProjEffects.SetFloatParameter(name("InflateBlob"), ChargePercent); } } simulated function SyncOriginalLocation() { local Actor HitActor; local vector HitLocation, HitNormal; local TraceHitInfo HitInfo; if (Role < ROLE_Authority && Instigator != none && Instigator.IsLocallyControlled()) { HitActor = Trace(HitLocation, HitNormal, OriginalLocation, Location,,, HitInfo, TRACEFLAG_Bullet); if (HitActor != none) { ProcessBulletTouch(HitActor, HitLocation, HitNormal); } } Super.SyncOriginalLocation(); } reliable client function SpawnImpactDecal(Vector HitLocation, vector HitNormal, float ChargePercentage ) { local float DecalSize; if( WorldInfo.MyDecalManager != none) { DecalSize = Lerp(ImpactDecalMinSize, ImpactDecalMaxSize, ChargePercentage); //DrawDebugSphere(ProjEffects.GetPosition(), 12, 6, 0, 255, 0, true); WorldInfo.MyDecalManager.SpawnDecal( ImpactDecalMaterial, HitLocation, rotator(-HitNormal) , DecalSize, DecalSize, ImpactDecalThickness, true ); } } defaultproperties { TerminalVelocity=5000 TossZ=150 GravityScale=0.5 MomentumTransfer=50000.0 LifeSpan=300 PostExplosionLifetime=1 Physics=PHYS_Falling bBounce=true ProjFlightTemplate= ParticleSystem'WEP_HRG_BallisticBouncer_EMIT.FX_HRG_BallisticBouncer_Ball_Projectile' BurstFXTemplate= ParticleSystem'WEP_HRG_BallisticBouncer_EMIT.FX_HRG_BallisticBouncer_Ball_Explode' HitFXTemplate= ParticleSystem'WEP_HRG_BallisticBouncer_EMIT.FX_HRG_BallisticBouncer_Ball_Hit' bSuppressSounds=false bAmbientSoundZedTimeOnly=false bAutoStartAmbientSound=false bStopAmbientSoundOnExplode=true ImpactAkEvent=AkEvent'WW_WEP_HRG_BallisticBouncer.Play_WEP_HRG_BallisticBouncer_Ball_Impact' BounceAkEvent=AkEvent'WW_WEP_HRG_BallisticBouncer.Play_WEP_HRG_BallisticBouncer_Ball_Impact' ImpactAkEventHeavy=AkEvent'WW_WEP_HRG_BallisticBouncer.Play_WEP_HRG_BallisticBouncer_Ball_Impact_Heavy' BounceAkEventHeavy=AkEvent'WW_WEP_HRG_BallisticBouncer.Play_WEP_HRG_BallisticBouncer_Ball_Impact_Heavy' BurstAkEvent=AkEvent'WW_WEP_HRG_BallisticBouncer.Play_WEP_HRG_BallisticBouncer_Ball_Explosion' Begin Object Class=AkComponent name=AmbientAkSoundComponent bStopWhenOwnerDestroyed=true bForceOcclusionUpdateInterval=true OcclusionUpdateInterval=0.25f End Object AmbientComponent=AmbientAkSoundComponent Components.Add(AmbientAkSoundComponent) ImpactDecalMaterial=DecalMaterial'WEP_HRG_BallisticBouncer_EMIT.FX_Ball_Impact_DM' ImpactDecalMinSize=20.f ImpactDecalMaxSize=80.f ImpactDecalThickness=28.f Begin Object Name=CollisionCylinder CollisionRadius=0.f CollisionHeight=0.f CollideActors=true BlockNonZeroExtent=false PhysMaterialOverride=PhysicalMaterial'WEP_HRG_BallisticBouncer_EMIT.BloatPukeMine_PM' End Object bCollideComplex=TRUE // Ignore simple collision on StaticMeshes, and collide per poly bUseClientSideHitDetection=true bNoReplicationToInstigator=false bUpdateSimulatedPosition=true bProjTarget=true bCanBeDamaged=false bNoEncroachCheck=true bPushedByEncroachers=false DampenFactor=0.175f DampenFactorParallel=0.175f ExtraLineCollisionOffsets.Add((Y=-20)) ExtraLineCollisionOffsets.Add((Y=20)) ExtraLineCollisionOffsets.Add((Z=-20)) ExtraLineCollisionOffsets.Add((Z=20)) // Since we're still using an extent cylinder, we need a line at 0 ExtraLineCollisionOffsets.Add(()) GlassShatterType=FMGS_ShatterAll InheritedScale=1 MaxInitialSpeedByCharge=5000 MinInitialSpeedByCharge=1500 MaxCollisionRadius=20 MinCollisionRadius=10 MaxCollisionHeight=20 MinCollisionHeight=10 MaxScalePerPercentage=1.5f MinScalePerPercentage=0.5f MaxBouncesPerPercentage=5 MinBouncesPerPercentage=1 MaxLifespanPerPercentage=500 MinLifeSpanPerPercentage=300 bBlockedByInstigator=true bNetTemporary=false bSyncToOriginalLocation=true bSyncToThirdPersonMuzzleLocation=true bReplicateLocationOnExplosion=true TouchTimeThreshhold=0.05 MaxBounces=0 NumBounces=0 ArmDistSquared=0 // Fade out properties FadeOutTime=5.0f // ImpactEffects= KFImpactEffectInfo'WEP_DragonsBreath_ARCH.DragonsBreath_bullet_impact' }