1
0
KF2-Dev-Scripts/KFGameContent/Classes/KFProj_HRG_BallisticBouncer.uc
2023-02-24 20:06:51 +03:00

628 lines
17 KiB
Ucode

//=============================================================================
// 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<Pawn> 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<ImpactInfo> 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'
}