1180 lines
42 KiB
Ucode
1180 lines
42 KiB
Ucode
|
//=============================================================================
|
||
|
// KFGoreManager
|
||
|
//=============================================================================
|
||
|
// Manager class for gore related effects
|
||
|
//=============================================================================
|
||
|
// Killing Floor 2
|
||
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
||
|
// - Sakib Saikia 12/3/2012
|
||
|
//=============================================================================
|
||
|
|
||
|
class KFGoreManager extends Actor
|
||
|
native(Effect)
|
||
|
config(Game);
|
||
|
|
||
|
`include(KFProfileSettings.uci);
|
||
|
|
||
|
/**
|
||
|
* Predefined values
|
||
|
*/
|
||
|
`define BLOOD_SPLATTER_TRACE_LENGTH 500.f
|
||
|
`define BLOOD_POOL_TRACE_LENGTH 250.f
|
||
|
`define WOUND_DECAL_GORE_LEVEL 1
|
||
|
`define BLOOD_SPLATTER_GORE_LEVEL 1
|
||
|
`define BLOOD_POOL_GORE_LEVEL 1
|
||
|
`define MUTILATION_GORE_LEVEL 0
|
||
|
`define HEADLESS_GORE_LEVEL 2
|
||
|
`define GIB_SPLAT_COOLDOWN 0.08
|
||
|
|
||
|
/**
|
||
|
* Transient Values managed outside of class
|
||
|
*/
|
||
|
var transient int DesiredGoreLevel;
|
||
|
|
||
|
/**
|
||
|
* Effect Lifetimes
|
||
|
*/
|
||
|
var globalconfig float GoreFXLifetimeMultiplier;
|
||
|
var globalconfig float BodyWoundDecalLifetime;
|
||
|
var globalconfig float BloodSplatterLifetime;
|
||
|
var globalconfig float BloodPoolLifetime;
|
||
|
var globalconfig float GibletLifetime;
|
||
|
|
||
|
/**
|
||
|
* Effect Dimensions
|
||
|
*/
|
||
|
var globalconfig float BloodSplatSize;
|
||
|
var globalconfig float BloodPoolSize;
|
||
|
|
||
|
/**
|
||
|
* Decal Limits
|
||
|
*/
|
||
|
var globalconfig int MaxBodyWoundDecals;
|
||
|
var globalconfig int MaxBloodSplatterDecals;
|
||
|
var globalconfig int MaxBloodPoolDecals;
|
||
|
var globalconfig bool bAllowBloodSplatterDecals;
|
||
|
|
||
|
/**
|
||
|
* Decal managers
|
||
|
*/
|
||
|
var transient DecalManager BodyWoundDecalManager;
|
||
|
var transient DecalManager BloodSplatterDecalManager;
|
||
|
var transient DecalManager BloodPoolDecalManager;
|
||
|
|
||
|
/**
|
||
|
* Emitter Limits
|
||
|
*/
|
||
|
var globalconfig int MaxBloodEffects;
|
||
|
var globalconfig int MaxGoreEffects;
|
||
|
|
||
|
/**
|
||
|
* Emitter Pools
|
||
|
*/
|
||
|
var transient EmitterPool BloodFXEmitterPool;
|
||
|
var transient EmitterPool MiscGoreFXEmitterPool;
|
||
|
|
||
|
/**
|
||
|
* Dead Pawns
|
||
|
*/
|
||
|
var array<KFPawn> CorpsePool;
|
||
|
var globalconfig int MaxDeadBodies;
|
||
|
var float MaxCorpseOffscreenTime;
|
||
|
var float MaxCorpseOffscreenDistance;
|
||
|
|
||
|
/**
|
||
|
* Persistent Splatters
|
||
|
*/
|
||
|
var globalconfig float PersistentSplatTraceLength;
|
||
|
var globalconfig int MaxPersistentSplatsPerFrame;
|
||
|
|
||
|
struct native PersistentSplatInfo
|
||
|
{
|
||
|
var vector Location;
|
||
|
var vector Normal;
|
||
|
var float Scale;
|
||
|
var bool bRandomize;
|
||
|
var float TraceLength;
|
||
|
|
||
|
structcpptext
|
||
|
{
|
||
|
FPersistentSplatInfo() {}
|
||
|
|
||
|
FPersistentSplatInfo(FVector InLocation, FVector InNormal, FLOAT InScale, UBOOL InRandomize, FLOAT TraceLength) :
|
||
|
Location(InLocation),
|
||
|
Normal(InNormal),
|
||
|
Scale(InScale),
|
||
|
bRandomize(InRandomize),
|
||
|
TraceLength(TraceLength) {}
|
||
|
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/* Cyclic buffer that contains the persistent splat generators for this frame */
|
||
|
var transient array<PersistentSplatInfo> CurrentSplats;
|
||
|
var transient int CurrentSplatIdx;
|
||
|
|
||
|
var transient array<TWSplatterMapTexture2D> CachedSplattermaps;
|
||
|
|
||
|
/**
|
||
|
* Debugging
|
||
|
*/
|
||
|
var bool bShowPersistentBloodTraces; // Set to TRUE if you want to see the line traces
|
||
|
var bool bLogGore;
|
||
|
|
||
|
event PostBeginPlay()
|
||
|
{
|
||
|
local KFGameEngine KFGE;
|
||
|
local KFProfileSettings KFPS;
|
||
|
Super.PostBeginPlay();
|
||
|
|
||
|
if(!class'WorldInfo'.static.IsConsoleBuild())
|
||
|
{
|
||
|
// Legacy support
|
||
|
DesiredGoreLevel = class'GameInfo'.default.GoreLevel;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
KFGE = KFGameEngine(class'Engine'.static.GetEngine());
|
||
|
KFPS = KFProfileSettings(KFGE.OnlineSubsystem.PlayerInterface.GetProfileSettings(KFGE.GamePlayers[0].ControllerId));
|
||
|
|
||
|
DesiredGoreLevel = KFPS.GetProfileInt(KFID_GoreLevel);
|
||
|
`QAlog(`location@`showvar(KFGE)@`showvar(KFPS)@`showvar(DesiredGoreLevel), true);
|
||
|
}
|
||
|
|
||
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
||
|
{
|
||
|
// Wound decal manager
|
||
|
BodyWoundDecalManager = Spawn(class'DecalManager', self,, vect(0,0,0), rot(0,0,0));
|
||
|
BodyWoundDecalManager.MaxActiveDecals = MaxBodyWoundDecals;
|
||
|
|
||
|
// Blood splatter decal manager
|
||
|
BloodSplatterDecalManager = Spawn(class'DecalManager', self,, vect(0,0,0), rot(0,0,0));
|
||
|
BloodSplatterDecalManager.MaxActiveDecals = MaxBloodSplatterDecals;
|
||
|
|
||
|
// Blood pool decal manager
|
||
|
BloodPoolDecalManager = Spawn(class'DecalManager', self,, vect(0,0,0), rot(0,0,0));
|
||
|
BloodPoolDecalManager.MaxActiveDecals = MaxBloodPoolDecals;
|
||
|
|
||
|
// Emitter pool for blood FX such as jets, trails, etc.
|
||
|
// Note : Blood spray is handled as part of the impact effect system. See PlayImpactParticleEffect()
|
||
|
BloodFXEmitterPool = Spawn(class'EmitterPool', self,, vect(0,0,0), rot(0,0,0));
|
||
|
BloodFXEmitterPool.MaxActiveEffects = MaxBloodEffects;
|
||
|
|
||
|
// Emitter pool for misc gore FX such as obliteration, custom dismemberment FX, etc.
|
||
|
MiscGoreFXEmitterPool = Spawn(class'EmitterPool', self,, vect(0,0,0), rot(0,0,0));
|
||
|
MiscGoreFXEmitterPool.MaxActiveEffects = MaxGoreEffects;
|
||
|
|
||
|
// clamp settings
|
||
|
MaxDeadBodies = Max(MaxDeadBodies, 4);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Body Wound
|
||
|
*********************************************************************************************/
|
||
|
|
||
|
simulated function LeaveABodyWoundDecal(KFpawn InPawn, vector InHitLocation, vector InHitDirection, name InHitZone, name InHitBone, class<KFDamageType> DmgType)
|
||
|
{
|
||
|
local vector HitDirection;
|
||
|
local vector DecalProjLocation;
|
||
|
local rotator DecalOrientation;
|
||
|
local DecalComponent WoundDecal;
|
||
|
local MaterialInterface WoundMIC;
|
||
|
local SkeletalMeshComponent HitComponent;
|
||
|
local float BodyWoundDecalWidth, BodyWoundDecalHeight;
|
||
|
|
||
|
if( WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.bDropDetail )
|
||
|
return;
|
||
|
|
||
|
if( DesiredGoreLevel <= `WOUND_DECAL_GORE_LEVEL )
|
||
|
{
|
||
|
if ( DmgType != None && DmgType.default.BodyWoundDecalMaterials.length > 0 )
|
||
|
{
|
||
|
WoundMIC = DmgType.default.BodyWoundDecalMaterials[rand(DmgType.default.BodyWoundDecalMaterials.length)];
|
||
|
BodyWoundDecalWidth = DmgType.default.BodyWoundDecalWidth;
|
||
|
BodyWoundDecalHeight = DmgType.default.BodyWoundDecalHeight;
|
||
|
}
|
||
|
|
||
|
if( WoundMIC != None )
|
||
|
{
|
||
|
HitDirection = Normal(InHitDirection);
|
||
|
DecalProjLocation = InHitLocation - 10*HitDirection; // Move the projection location slightly away from the actual impact location
|
||
|
DecalOrientation = rotator(-HitDirection);
|
||
|
|
||
|
// Pick the right mesh basedon the hit zone
|
||
|
if( (InHitZone == 'head' || InHitZone == 'neck') &&
|
||
|
InPawn.ThirdPersonHeadMeshComponent != none )
|
||
|
{
|
||
|
HitComponent = InPawn.ThirdPersonHeadMeshComponent;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
HitComponent = InPawn.Mesh;
|
||
|
}
|
||
|
|
||
|
WoundDecal = BodyWoundDecalManager.SpawnDecal(
|
||
|
WoundMIC,
|
||
|
DecalProjLocation, DecalOrientation,
|
||
|
BodyWoundDecalWidth, BodyWoundDecalHeight, 50.0f,
|
||
|
false,, HitComponent,
|
||
|
false, true, InHitBone,
|
||
|
,, BodyWoundDecalLifetime * GoreFXLifetimeMultiplier);
|
||
|
|
||
|
WoundDecal.bNoClip = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Blood Splatter
|
||
|
*********************************************************************************************/
|
||
|
|
||
|
/** Leaves a persistent blood splat on level geometry
|
||
|
bForceUpdate will force immediate update instead of queueing it up in the cyclic buffer
|
||
|
*/
|
||
|
simulated native final function LeaveAPersistentBloodSplat(vector HitLoc, vector HitNorm, optional float BloodScale = 1.0, optional bool bRandomizeBloodScale = true, optional bool bForceUpdate = false, optional float TraceLength = PersistentSplatTraceLength);
|
||
|
|
||
|
/** Helper function for LeaveAPersistentBloodSplat. Traces agains geometry and updates the splattermaps */
|
||
|
simulated native final function PerformTraceAndUpdateSplattermap(const out PersistentSplatInfo InSplat);
|
||
|
|
||
|
/** Clears all persistent blood splats in the world. The splats are automatically cleared on map reload.
|
||
|
Use this function only if you want to clear them during the match. */
|
||
|
native final function ClearPersistentBloodSplats();
|
||
|
|
||
|
/** Cache the splattermaps currently used by the level */
|
||
|
native final function CacheCurrentSplattermaps();
|
||
|
|
||
|
/** Perform traces and update splattermap textures */
|
||
|
native final function FlushPersistentBloodSplats();
|
||
|
|
||
|
/** Spawn persistent blood effects */
|
||
|
simulated function CausePersistentBlood(KFPawn_Monster InPawn, class<KFDamageType> InDmgType, vector InHitLocation, vector InHitDirection, int InHitZoneIndex, bool bIsDismeberingHit, bool bWasObliterated)
|
||
|
{
|
||
|
local array<vector> HitSpread;
|
||
|
local float BloodScale;
|
||
|
local int i;
|
||
|
|
||
|
//If we have an invalid hit zone, don't spawn blood
|
||
|
if (InHitZoneIndex > InPawn.HitZones.Length)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//Allow damage type to add more spread to the hit direction based its type.
|
||
|
HitSpread.Remove(0, HitSpread.length);
|
||
|
InDmgType.static.AddBloodSpread(InPawn, InHitDirection, HitSpread, bIsDismeberingHit, bWasObliterated);
|
||
|
|
||
|
// Allow damage type to modify the blood scale to be applied
|
||
|
// (taking into account the damage scale of the hit zone)
|
||
|
// Note: if we need more control over the scale we can decouple it from
|
||
|
// damage scale and use a custom "BloodScale" for each hit zone.
|
||
|
BloodScale = InDmgType.static.GetBloodScale(InHitZoneIndex != 255 ? InPawn.HitZones[InHitZoneIndex].DmgScale : 1.f, bIsDismeberingHit, bWasObliterated);
|
||
|
|
||
|
for( i=0; i<HitSpread.length; i++ )
|
||
|
{
|
||
|
LeaveAPersistentBloodSplat(InHitLocation, HitSpread[i], BloodScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Leaves a temporary blood decal on level geometry */
|
||
|
simulated function LeaveABloodSplatterDecal(KFPawn InPawn, vector HitLoc, vector HitNorm )
|
||
|
{
|
||
|
local Actor TraceActor;
|
||
|
local vector TraceHitLoc, TraceHitNorm;
|
||
|
local vector TraceDest, TraceStart;
|
||
|
local vector TraceExtent;
|
||
|
local TraceHitInfo HitInfo;
|
||
|
local int DecalScaler;
|
||
|
|
||
|
if( WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.bDropDetail || !bAllowBloodSplatterDecals )
|
||
|
return;
|
||
|
|
||
|
if( DesiredGoreLevel <= `BLOOD_SPLATTER_GORE_LEVEL )
|
||
|
{
|
||
|
TraceStart = HitLoc;
|
||
|
TraceDest = HitLoc + normal(HitNorm) * `BLOOD_SPLATTER_TRACE_LENGTH;
|
||
|
|
||
|
TraceActor = Trace(TraceHitLoc, TraceHitNorm, TraceDest, TraceStart, false, TraceExtent, HitInfo, TRACEFLAG_PhysicsVolumes );
|
||
|
|
||
|
// START DEBUG
|
||
|
// DrawDebugLine(TraceDest, TraceStart, 255, 0, 0, true);
|
||
|
// DrawDebugSphere(TraceStart, 5, 20, 0, 255, 0, true);
|
||
|
// DrawDebugSphere(TraceDest, 5, 20, 0, 0, 255, true);
|
||
|
// END DEBUG
|
||
|
|
||
|
if (TraceActor != None && Pawn(TraceActor) == None)
|
||
|
{
|
||
|
DecalScaler = Rand(50);
|
||
|
BloodSplatterDecalManager.SpawnDecal(
|
||
|
InPawn.BloodSplatterDecalMaterials[Rand(InPawn.BloodSplatterDecalMaterials.Length)],
|
||
|
TraceHitLoc, rotator(-TraceHitNorm),
|
||
|
BloodSplatSize + DecalScaler, BloodSplatSize + DecalScaler, 50,
|
||
|
false,,,,,,,,
|
||
|
BloodSplatterLifetime * GoreFXLifetimeMultiplier);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
simulated function float GetTimeBetweenGibBloodSplats()
|
||
|
{
|
||
|
return `GIB_SPLAT_COOLDOWN;
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Blood Pool
|
||
|
*********************************************************************************************/
|
||
|
|
||
|
simulated function LeaveABloodPoolDecal(KFPawn InPawn)
|
||
|
{
|
||
|
local Actor TraceActor;
|
||
|
local vector TraceHitLocation, TraceHitNormal;
|
||
|
local vector TraceDest, TraceStart;
|
||
|
local TraceHitInfo HitInfo;
|
||
|
local MaterialInstanceTimeVarying MITV_Decal;
|
||
|
local int DecalScaler;
|
||
|
local name OriginBone;
|
||
|
|
||
|
if( WorldInfo.bDropDetail )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if( DesiredGoreLevel <= `BLOOD_POOL_GORE_LEVEL )
|
||
|
{
|
||
|
// If given bone is valid, trace from its world space location. Otherwise, trace from root bone
|
||
|
OriginBone = InPawn.CharacterArch.BloodPoolOriginBoneName;
|
||
|
if( OriginBone != '' && InPawn.mesh.MatchRefBone(OriginBone) != INDEX_NONE )
|
||
|
{
|
||
|
TraceStart = InPawn.mesh.GetBoneLocation(OriginBone);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
TraceStart = InPawn.mesh.GetBoneLocation(InPawn.mesh.GetBoneName(0));
|
||
|
}
|
||
|
|
||
|
TraceDest = TraceStart + `BLOOD_POOL_TRACE_LENGTH * vect(0,0,-1);
|
||
|
|
||
|
TraceActor = Trace(TraceHitLocation, TraceHitNormal, TraceDest, TraceStart, false,, HitInfo, TRACEFLAG_PhysicsVolumes);
|
||
|
|
||
|
if( TraceActor != none && Pawn(TraceActor) == none )
|
||
|
{
|
||
|
MITV_Decal = new(Outer) class'MaterialInstanceTimeVarying';
|
||
|
MITV_Decal.SetParent(InPawn.BloodPoolDecalMaterials[Rand(InPawn.BloodPoolDecalMaterials.Length)]);
|
||
|
MITV_Decal.SetDuration(BloodPoolLifetime * GoreFXLifetimeMultiplier);
|
||
|
|
||
|
DecalScaler = Rand(25);
|
||
|
BloodPoolDecalManager.SpawnDecal(
|
||
|
MITV_Decal,
|
||
|
TraceHitLocation, rotator(-TraceHitNormal),
|
||
|
BloodPoolSize + DecalScaler, BloodPoolSize + DecalScaler, 32,
|
||
|
false,,,true,false,,,,
|
||
|
BloodPoolLifetime * GoreFXLifetimeMultiplier);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Dismemberment
|
||
|
*********************************************************************************************/
|
||
|
simulated final function bool AllowMutilation()
|
||
|
{
|
||
|
return DesiredGoreLevel <= `MUTILATION_GORE_LEVEL;
|
||
|
}
|
||
|
|
||
|
simulated final function bool AllowHeadless()
|
||
|
{
|
||
|
return DesiredGoreLevel <= `HEADLESS_GORE_LEVEL;
|
||
|
}
|
||
|
|
||
|
/** Spawns blood effects due to limb dismemberment or gibbing
|
||
|
@InPawn Affected pawn
|
||
|
@DismemberedBone Bone used to calculate attachment transform, etc.
|
||
|
@BloodJets Species the blood jet behavior
|
||
|
@BloodTrails Species the blood trail behavior
|
||
|
@BloodMICParams Species the blood overlay behavior
|
||
|
*/
|
||
|
simulated final function AttachMutilationBloodEffects(
|
||
|
KFPawn_Monster InPawn,
|
||
|
Name DismemberedBone,
|
||
|
optional array<BloodJetSettings> BloodJets,
|
||
|
optional array<BloodTrailSettings> BloodTrails,
|
||
|
optional array<name> BloodMICParams)
|
||
|
{
|
||
|
local name ParentBone;
|
||
|
local int ParentBoneIndex, DismemberedBoneIndex, BloodParamIndex, BloodJetIndex, BloodTrailIndex, MICIndex;
|
||
|
local SkeletalMeshComponent SkelMesh;
|
||
|
local vector OffsetFromParentBone, TranslationFromParentBone, ParentBoneFaceDir;
|
||
|
local bool bOppositeFacing;
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
|
||
|
if( WorldInfo.NetMode == NM_DedicatedServer )
|
||
|
return;
|
||
|
|
||
|
if( InPawn != none && InPawn.mesh != none )
|
||
|
{
|
||
|
MICIndex = 0;
|
||
|
if (InPawn.CharacterMICs.Length > InPawn.GetCharacterInfo().GoreFXMICIdx)
|
||
|
{
|
||
|
MICIndex = InPawn.GetCharacterInfo().GoreFXMICIdx;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Activate the blood splat on the body MIC
|
||
|
// NOTE: 0.f activates the blood 1.f deactivates it
|
||
|
//
|
||
|
for( BloodParamIndex = 0; BloodParamIndex < BloodMICParams.length; BloodParamIndex++ )
|
||
|
{
|
||
|
InPawn.CharacterMICs[MICIndex].SetScalarParameterValue(BloodMICParams[BloodParamIndex], 0.f);
|
||
|
}
|
||
|
|
||
|
if ( WorldInfo.bDropDetail )
|
||
|
{
|
||
|
return; // MIC params only
|
||
|
}
|
||
|
|
||
|
SkelMesh = InPawn.mesh;
|
||
|
ParentBone = SkelMesh.GetParentBone(DismemberedBone);
|
||
|
DismemberedBoneIndex = SkelMesh.MatchRefBone(DismemberedBone);
|
||
|
ParentBoneIndex = SkelMesh.MatchRefBone(ParentBone);
|
||
|
MonsterInfo = InPawn.GetCharacterMonsterInfo();
|
||
|
|
||
|
if( DismemberedBoneIndex != INDEX_None && ParentBoneIndex != INDEX_NONE && MonsterInfo != none )
|
||
|
{
|
||
|
// Get the offset of the dismembered bone from the parent bone. We need to attach
|
||
|
// the blood to the parent bone but offset to the position where the dismembered bone was.
|
||
|
OffsetFromParentBone = SkelMesh.LocalAtoms[DismemberedBoneIndex].Translation;
|
||
|
|
||
|
// Because the bones on the left and right side of the character are flipped in direction
|
||
|
// we need to procedurally figure out which way the parent bone is facing. If the parent bone
|
||
|
// is facing away from the dismembered bone, we need to flip the rotation of the attached jet.
|
||
|
TranslationFromParentBone = SkelMesh.SpaceBases[DismemberedBoneIndex].Translation - SkelMesh.SpaceBases[ParentBoneIndex].Translation;
|
||
|
ParentBoneFaceDir = SkelMesh.GetBoneAxis(ParentBone, AXIS_X);
|
||
|
bOppositeFacing = normal(TranslationFromParentBone) dot ParentBoneFaceDir < 0.f ? true : false;
|
||
|
|
||
|
//
|
||
|
// Attach blood jets
|
||
|
//
|
||
|
for( BloodJetIndex = 0; BloodJetIndex < BloodJets.length; BloodJetIndex++ )
|
||
|
{
|
||
|
if( BloodJets[BloodJetIndex].bAttachToSocket )
|
||
|
{
|
||
|
BloodFXEmitterPool.SpawnEmitterMeshAttachment(
|
||
|
BloodJets[BloodJetIndex].ParticleSystemTemplate,
|
||
|
SkelMesh,
|
||
|
BloodJets[BloodJetIndex].SocketName,
|
||
|
true);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
BloodFXEmitterPool.SpawnEmitterMeshAttachment(
|
||
|
BloodJets[BloodJetIndex].ParticleSystemTemplate,
|
||
|
SkelMesh, ParentBone, false,
|
||
|
OffsetFromParentBone,
|
||
|
bOppositeFacing ? rot(0,32768,0) : rot(0,0,0));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Attach blood trail to dismembered bone
|
||
|
//
|
||
|
for( BloodTrailIndex = 0; BloodTrailIndex < BloodTrails.length; BloodTrailIndex++ )
|
||
|
{
|
||
|
if( BloodTrails[BloodTrailIndex].bAttachToSocket )
|
||
|
{
|
||
|
BloodFXEmitterPool.SpawnEmitterMeshAttachment(
|
||
|
BloodTrails[BloodTrailIndex].ParticleSystemTemplate,
|
||
|
SkelMesh,
|
||
|
BloodTrails[BloodTrailIndex].SocketName,
|
||
|
true);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
BloodFXEmitterPool.SpawnEmitterMeshAttachment(
|
||
|
BloodTrails[BloodTrailIndex].ParticleSystemTemplate,
|
||
|
SkelMesh, DismemberedBone, false,,
|
||
|
bOppositeFacing ? rot(0,0,0) : rot(0,32768,0));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Breaks off a joint in the physics asset of the pawn's mesh
|
||
|
@return true if successful, or false if the constraint cannot be broken or is already broken
|
||
|
*/
|
||
|
simulated final function bool BreakConstraint(
|
||
|
KFPawn_Monster InPawn,
|
||
|
Name InBoneName,
|
||
|
optional class<KFDamageType> InDmgType,
|
||
|
optional bool bToggleWeightsOnly = false)
|
||
|
{
|
||
|
local int JointIndex, DependencyIdx, DependentBoneIdx;
|
||
|
local DependentBreakSettings CurrentBreakDependency;
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
local ParticleSystemComponent PSC;
|
||
|
|
||
|
if( WorldInfo.NetMode == NM_DedicatedServer )
|
||
|
return false;
|
||
|
|
||
|
if( InPawn.mesh == none )
|
||
|
{
|
||
|
`if(`notdefined(ShippingPC))
|
||
|
`warn(WorldInfo.TimeSeconds @ self @ GetFuncName() @ "Tried to dismember a mesh with no gore skeleton!!");
|
||
|
ScriptTrace();
|
||
|
`endif
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
MonsterInfo = InPawn.GetCharacterMonsterInfo();
|
||
|
|
||
|
if( InPawn != none &&
|
||
|
!InPawn.Mesh.IsBrokenConstraint(InBoneName) &&
|
||
|
MonsterInfo != none )
|
||
|
{
|
||
|
// Keep track that broke at least one constraint off this gore mesh.
|
||
|
InPawn.bHasBrokenConstraints = TRUE;
|
||
|
|
||
|
if( bToggleWeightsOnly )
|
||
|
{
|
||
|
// switch to alternate weights only
|
||
|
InPawn.Mesh.ToggleAlternateBoneWeights(InBoneName);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// break primary constraint and switch to alternate weights
|
||
|
InPawn.Mesh.BreakConstraint(InBoneName);
|
||
|
}
|
||
|
|
||
|
// Attach a gore chunk if specified
|
||
|
InPawn.HandleGoreChunkAttachments(InBoneName);
|
||
|
|
||
|
if ( InDmgType != None )
|
||
|
{
|
||
|
for( JointIndex = 0; JointIndex < MonsterInfo.GoreJointSettings.length; JointIndex++ )
|
||
|
{
|
||
|
if( MonsterInfo.GoreJointSettings[JointIndex].HitBoneName == InBoneName )
|
||
|
{
|
||
|
for( DependencyIdx = 0;
|
||
|
DependencyIdx < MonsterInfo.GoreJointSettings[JointIndex].DependentBreakGore.length;
|
||
|
DependencyIdx++ )
|
||
|
{
|
||
|
CurrentBreakDependency = MonsterInfo.GoreJointSettings[JointIndex].DependentBreakGore[DependencyIdx];
|
||
|
|
||
|
// If the dependancy is either not constrained to any damage types or
|
||
|
// has the damage type in the list of constraints
|
||
|
if( (CurrentBreakDependency.ConstrainToDamageGroups.length == 0 ||
|
||
|
CurrentBreakDependency.ConstrainToDamageGroups.Find(DGT_None) != INDEX_None ||
|
||
|
CurrentBreakDependency.ConstrainToDamageGroups.Find(InDmgType.default.GoreDamageGroup) != INDEX_None))
|
||
|
{
|
||
|
for( DependentBoneIdx = 0;
|
||
|
DependentBoneIdx < CurrentBreakDependency.DependentBones.length;
|
||
|
DependentBoneIdx++)
|
||
|
{
|
||
|
//`log(GetFuncName()@"Breaking "@CurrentBreakDependency.DependentBones[DependentBoneIdx].BoneName@"because"@InBoneName@"was broken!");
|
||
|
InPawn.Mesh.BreakConstraint(CurrentBreakDependency.DependentBones[DependentBoneIdx].BoneName);
|
||
|
|
||
|
// Blood effects
|
||
|
AttachMutilationBloodEffects(
|
||
|
InPawn,
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].BoneName,
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].BloodJets,
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].BloodTrails,
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].BloodMICParamName);
|
||
|
|
||
|
// Spawn additional effect if specified
|
||
|
if( CurrentBreakDependency.DependentBones[DependentBoneIdx].ParticleSystemTemplate != none )
|
||
|
{
|
||
|
// NVCHANGE_BEGIN: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
PSC = MiscGoreFXEmitterPool.SpawnEmitter(
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].ParticleSystemTemplate,
|
||
|
InPawn.mesh.GetBoneLocation(
|
||
|
CurrentBreakDependency.DependentBones[DependentBoneIdx].BoneName)
|
||
|
);
|
||
|
PSC.SetLightingChannels(InPawn.PawnLightingChannel);
|
||
|
// NVCHANGE_END: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
}
|
||
|
|
||
|
// Attach a gore chunk if specified
|
||
|
InPawn.HandleGoreChunkAttachments(CurrentBreakDependency.DependentBones[DependentBoneIdx].BoneName);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
/** Break-off, hide bone, and play associated effects */
|
||
|
simulated function CrushBone(KFPawn_Monster InPawn, name InHitBoneName)
|
||
|
{
|
||
|
if ( AllowMutilation() )
|
||
|
{
|
||
|
// For simplicity, just call the 'conditional' version with bForceApply=true
|
||
|
ConditionalApplyPartialGore(InPawn, none, InPawn.mesh.GetBoneLocation(InHitBoneName), vect(0,0,0), InHitBoneName, true);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// shrink bone without gore FX
|
||
|
if( BreakConstraint(InPawn, InHitBoneName,,true) )
|
||
|
{
|
||
|
InPawn.mesh.HideBoneByName(InHitBoneName, PBO_Term);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Check if partial gore can be applied on the hit bone for the specified damage type.
|
||
|
If yes, then apply by chipping one or more of the partial break bones.
|
||
|
@return true if partial gore was successfully applied
|
||
|
*/
|
||
|
simulated function bool ConditionalApplyPartialGore(
|
||
|
KFPawn_Monster InPawn,
|
||
|
class<KFDamageType> InDmgType,
|
||
|
vector InHitLocation,
|
||
|
vector InHitDirection,
|
||
|
name InHitBoneName,
|
||
|
optional bool bForceApply)
|
||
|
{
|
||
|
local int i, JointIndex, PartialBreakIndex, ClosestBoneIndex;
|
||
|
local name ClosestBone;
|
||
|
local array<name> ClosestBoneList;
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
local PartialBreakSettings CurrentPartialBreak;
|
||
|
local array<name> SearchBoneList;
|
||
|
local ParticleSystemComponent PSC;
|
||
|
|
||
|
MonsterInfo = InPawn.GetCharacterMonsterInfo();
|
||
|
|
||
|
// Find the gore joint settings corresponding to the hit bone
|
||
|
for( JointIndex = 0; JointIndex < MonsterInfo.GoreJointSettings.length; JointIndex++ )
|
||
|
{
|
||
|
if( MonsterInfo.GoreJointSettings[JointIndex].HitBoneName == InHitBoneName )
|
||
|
{
|
||
|
// Check all the partial breaks of the hit bone
|
||
|
for( PartialBreakIndex = 0;
|
||
|
PartialBreakIndex < MonsterInfo.GoreJointSettings[JointIndex].BoneShrinkGore.length;
|
||
|
PartialBreakIndex++ )
|
||
|
{
|
||
|
// Cache current partial break (for sanity)
|
||
|
CurrentPartialBreak = MonsterInfo.GoreJointSettings[JointIndex].BoneShrinkGore[PartialBreakIndex];
|
||
|
|
||
|
// If the particular partial break supports the specific damage type
|
||
|
if( bForceApply ||
|
||
|
(CurrentPartialBreak.ConstrainToDamageGroups.length == 0 ||
|
||
|
CurrentPartialBreak.ConstrainToDamageGroups.Find(DGT_None) != INDEX_None ||
|
||
|
CurrentPartialBreak.ConstrainToDamageGroups.Find(InDmgType.default.GoreDamageGroup) != INDEX_None
|
||
|
) ||
|
||
|
// This is the killing blow, and the killing blow damage type causes this partial break
|
||
|
( InPawn.TimeOfDeath == WorldInfo.TimeSeconds &&
|
||
|
( CurrentPartialBreak.KillingBlowDamageGroups.length == 0 ||
|
||
|
CurrentPartialBreak.KillingBlowDamageGroups.Find(DGT_None) != INDEX_None ||
|
||
|
CurrentPartialBreak.KillingBlowDamageGroups.Find(InDmgType.default.GoreDamageGroup) != INDEX_None
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
{
|
||
|
|
||
|
// Add all partial break bones for the hit bone to the proximity search list
|
||
|
for( i=0; i < CurrentPartialBreak.PartialBreakBones.length; i++ )
|
||
|
{
|
||
|
// Only add the hit bone to the list for the head if it isn't already
|
||
|
// broken or if the pawn will allow it. That way multiple shots to the
|
||
|
// head will chunk off different head chunks with every shot.
|
||
|
if( InHitBoneName != 'head' || (!InPawn.HeadBoneAlreadyBroken(CurrentPartialBreak.PartialBreakBones[i].BoneName)
|
||
|
&& InPawn.ShouldAllowHeadBoneToBreak(CurrentPartialBreak.PartialBreakBones[i].BoneName)) )
|
||
|
{
|
||
|
SearchBoneList.AddItem(CurrentPartialBreak.PartialBreakBones[i].BoneName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Find closest bone to hit location in the search list
|
||
|
InPawn.mesh.FindClosestBones(InHitLocation, 1, ClosestBoneList, SearchBoneList, 0.f);
|
||
|
|
||
|
if( ClosestBoneList.length > 0 )
|
||
|
{
|
||
|
// Retrieve the closest bone and it's index in the PartialBreakBones array
|
||
|
ClosestBone = ClosestBoneList[0];
|
||
|
ClosestBoneIndex = CurrentPartialBreak.PartialBreakBones.Find('BoneName', ClosestBone);
|
||
|
|
||
|
if( ClosestBoneIndex >= 0 )
|
||
|
{
|
||
|
// Don't let this head bone to break if the pawn won't allow it
|
||
|
if( InHitBoneName == 'head' && !InPawn.ShouldAllowHeadBoneToBreak(ClosestBone))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Note: Not checking for bNonBreakableJoint here because the partial
|
||
|
// gore bones are manually added and hence should support broken constraints.
|
||
|
if( BreakConstraint(InPawn, ClosestBone,,true) )
|
||
|
{
|
||
|
// Shrink bone
|
||
|
InPawn.mesh.HideBoneByName(ClosestBone, PBO_Term);
|
||
|
|
||
|
if( InHitBoneName == 'head' )
|
||
|
{
|
||
|
InPawn.AddBrokenHeadBone(ClosestBone);
|
||
|
}
|
||
|
|
||
|
// Play jet effect if specified
|
||
|
AttachMutilationBloodEffects(
|
||
|
InPawn, ClosestBone,
|
||
|
CurrentPartialBreak.PartialBreakBones[ClosestBoneIndex].BloodJets,
|
||
|
,
|
||
|
CurrentPartialBreak.PartialBreakBones[ClosestBoneIndex].BloodMICParamName);
|
||
|
|
||
|
// Play additional particle effect if specified
|
||
|
// @note: ignore bDropDetail for CrushBone() (aka bForceApply=TRUE) particle
|
||
|
if( CurrentPartialBreak.PartialBreakBones[ClosestBoneIndex].ParticleSystemTemplate != none
|
||
|
&& (!WorldInfo.bDropDetail || bForceApply) )
|
||
|
{
|
||
|
// NVCHANGE_BEGIN: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
PSC = MiscGoreFXEmitterPool.SpawnEmitter(
|
||
|
CurrentPartialBreak.PartialBreakBones[ClosestBoneIndex].ParticleSystemTemplate,
|
||
|
InHitLocation);
|
||
|
PSC.SetLightingChannels(InPawn.PawnLightingChannel);
|
||
|
// NVCHANGE_END: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
`log("ConditionalApplyPartialGore -- On Pawn: " $ InPawn @ " Trying to Work on a bone out of the array, ClosestBone: " $ ClosestBone $ " - InHitBoneName: " $ InHitBoneName, bLogGore);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** Dismemberment gore effects */
|
||
|
simulated function CauseDismemberment(KFPawn_Monster InPawn, name InHitBoneName, class<KFDamageType> InDmgType)
|
||
|
{
|
||
|
local int JointIndex, EffectIdx;
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
local DismembermentEffect CurrentEffect;
|
||
|
local ParticleSystemComponent PSC;
|
||
|
|
||
|
MonsterInfo = InPawn.GetCharacterMonsterInfo();
|
||
|
|
||
|
for( JointIndex = 0; JointIndex < MonsterInfo.GoreJointSettings.length; JointIndex++ )
|
||
|
{
|
||
|
if( MonsterInfo.GoreJointSettings[JointIndex].HitBoneName == InHitBoneName &&
|
||
|
!MonsterInfo.GoreJointSettings[JointIndex].bNonBreakableJoint )
|
||
|
{
|
||
|
if( BreakConstraint(InPawn, InHitBoneName, InDmgType) )
|
||
|
{
|
||
|
// Play blood effects
|
||
|
AttachMutilationBloodEffects(
|
||
|
InPawn, InHitBoneName,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodJets,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodTrails,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodMICParamName);
|
||
|
|
||
|
// Spawn any effects if specified
|
||
|
for( EffectIdx = 0;
|
||
|
EffectIdx < MonsterInfo.GoreJointSettings[JointIndex].DismembermentEffects.length;
|
||
|
EffectIdx++)
|
||
|
{
|
||
|
CurrentEffect = MonsterInfo.GoreJointSettings[JointIndex].DismembermentEffects[EffectIdx];
|
||
|
|
||
|
if( (CurrentEffect.ConstrainToDamageGroups.length == 0 ||
|
||
|
CurrentEffect.ConstrainToDamageGroups.Find(DGT_None) != INDEX_None ||
|
||
|
CurrentEffect.ConstrainToDamageGroups.Find(InDmgType.default.GoreDamageGroup) != INDEX_None) && !WorldInfo.bDropDetail )
|
||
|
{
|
||
|
// NVCHANGE_BEGIN: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
PSC = MiscGoreFXEmitterPool.SpawnEmitter(
|
||
|
CurrentEffect.ParticleSystemTemplate,
|
||
|
InPawn.mesh.GetBoneLocation(InHitBoneName));
|
||
|
PSC.SetLightingChannels(InPawn.PawnLightingChannel);
|
||
|
// NVCHANGE_END: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Gib gore effects - Explosive Damage Only */
|
||
|
simulated function CauseGibsAndApplyImpulse(
|
||
|
KFPawn_Monster InPawn,
|
||
|
class<KFDamageType> InDmgType,
|
||
|
vector InExplosionOrigin,
|
||
|
array<name> InGibBoneList,
|
||
|
ParticleSystem ExplosionEffect,
|
||
|
vector ExplosionEffectLocation,
|
||
|
optional name HitBoneName = 'None')
|
||
|
{
|
||
|
local vector Impulse, BoneLocation;
|
||
|
local int GibIdx, JointIndex, ExplosionBreakIdx, BoneIdx;
|
||
|
local Name GibBoneName;
|
||
|
local bool bBrokenConstraint, bPlayedBloodEffects;
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
local ExplosionBreakBone ExplosiveBreakBone;
|
||
|
local name RBBoneName;
|
||
|
local ParticleSystemComponent PSC;
|
||
|
local int NumGibs;
|
||
|
local float ModifiedImpulseLerpValue;
|
||
|
local float ModifiedImpulse;
|
||
|
local float GibImpulseMin, GibImpulseMax;
|
||
|
|
||
|
MonsterInfo = InPawn.GetCharacterMonsterInfo();
|
||
|
|
||
|
// We need to scale the impule values based on the # of gibs to disconnect otherwise their impulses are way
|
||
|
// to high and limbs go flying really far. We perform a linear interpolation
|
||
|
GibImpulseMax = InDmgType.default.GibImpulseScale;
|
||
|
GibImpulseMin = GibImpulseMax/2.0f;
|
||
|
NumGibs = InGibBoneList.Length;
|
||
|
ModifiedImpulseLerpValue = 1.0f - numGibs/ MonsterInfo.GoreJointSettings.length;
|
||
|
ModifiedImpulse = lerp(GibImpulseMin, GibImpulseMax, ModifiedImpulseLerpValue);
|
||
|
|
||
|
for( GibIdx=0; GibIdx<InGibBoneList.length; GibIdx++ )
|
||
|
{
|
||
|
GibBoneName = InGibBoneList[GibIdx];
|
||
|
|
||
|
BoneLocation = InPawn.mesh.GetBoneLocation(GibBoneName);
|
||
|
Impulse = InDmgType.default.RadialDamageImpulse * InDmgType.default.bPointImpulseTowardsOrigin
|
||
|
? Normal((InPawn.Location - InExplosionOrigin) + (vect(0,0,1) * InDmgType.default.ImpulseOriginLift))
|
||
|
: Normal(BoneLocation - InExplosionOrigin);
|
||
|
Impulse *= MonsterInfo.ExplosionImpulseScale*ModifiedImpulse;
|
||
|
|
||
|
for( JointIndex = 0; JointIndex < MonsterInfo.GoreJointSettings.length; JointIndex++ )
|
||
|
{
|
||
|
if( MonsterInfo.GoreJointSettings[JointIndex].HitBoneName == GibBoneName &&
|
||
|
!MonsterInfo.GoreJointSettings[JointIndex].bNonBreakableJoint )
|
||
|
{
|
||
|
// Play blood effects only if the joint was successfully broken
|
||
|
if( BreakConstraint(InPawn, GibBoneName, InDmgType) )
|
||
|
{
|
||
|
bBrokenConstraint = true;
|
||
|
|
||
|
// If this explosion is not triggered by a hit explosion joint (e.g. grenade explosion)
|
||
|
// then use settings blood effect settings from the joint's primary dismemberment
|
||
|
if( HitBoneName == 'None' ||
|
||
|
HitBoneName == '' )
|
||
|
{
|
||
|
AttachMutilationBloodEffects(
|
||
|
InPawn, GibBoneName,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodJets,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodTrails,
|
||
|
MonsterInfo.GoreJointSettings[JointIndex].BloodMICParamName);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
bPlayedBloodEffects = false;
|
||
|
|
||
|
// Look for the HitBoneName in the GoreJointSettings list
|
||
|
for( JointIndex = 0;
|
||
|
!bPlayedBloodEffects && JointIndex < MonsterInfo.GoreJointSettings.length;
|
||
|
JointIndex++ )
|
||
|
{
|
||
|
if( MonsterInfo.GoreJointSettings[JointIndex].HitBoneName == HitBoneName )
|
||
|
{
|
||
|
// Go through each hit explosion break for the specified hit bone name and try
|
||
|
// to find the GibBoneName in the list. Take blood effect settings from the first
|
||
|
// entry that matches the gib bone.
|
||
|
for( ExplosionBreakIdx = 0;
|
||
|
!bPlayedBloodEffects && ExplosionBreakIdx < MonsterInfo.GoreJointSettings[JointIndex].HitExplosionGore.length;
|
||
|
ExplosionBreakIdx++ )
|
||
|
{
|
||
|
for( BoneIdx = 0;
|
||
|
BoneIdx < MonsterInfo.GoreJointSettings[JointIndex].HitExplosionGore[ExplosionBreakIdx].BreakBones.length;
|
||
|
BoneIdx++ )
|
||
|
{
|
||
|
ExplosiveBreakBone = MonsterInfo.GoreJointSettings[JointIndex].HitExplosionGore[ExplosionBreakIdx].BreakBones[BoneIdx];
|
||
|
if( ExplosiveBreakBone.BoneName == GibBoneName )
|
||
|
{
|
||
|
AttachMutilationBloodEffects(
|
||
|
InPawn, GibBoneName,
|
||
|
ExplosiveBreakBone.BloodJets,
|
||
|
ExplosiveBreakBone.BloodTrails,
|
||
|
ExplosiveBreakBone.BloodMICParamName);
|
||
|
|
||
|
bPlayedBloodEffects = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get the rigidbody bone that matches the hit bone name
|
||
|
RBBoneName = InPawn.GetRBBoneFromBoneName(GibBoneName);
|
||
|
// Apply impulse after trying to break the constraint
|
||
|
InPawn.mesh.AddImpulse(Impulse, BoneLocation, RBBoneName);
|
||
|
}
|
||
|
|
||
|
// If an explosion effect has been specified, spawn it now.
|
||
|
// Do not spawn effect if no constraints were broken or if the effect has already been played.
|
||
|
if( bBrokenConstraint && !InPawn.bPlayedExplosionEffect && ExplosionEffect != none )
|
||
|
{
|
||
|
// NVCHANGE_BEGIN: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
PSC = MiscGoreFXEmitterPool.SpawnEmitter(ExplosionEffect, ExplosionEffectLocation);
|
||
|
PSC.SetLightingChannels(InPawn.PawnLightingChannel);
|
||
|
// NVCHANGE_END: JCAO - Apply the lightingChannel for the particle from the pawn
|
||
|
InPawn.bPlayedExplosionEffect = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Spawn a giblet caused by obliteration */
|
||
|
simulated function KFGiblet SpawnGiblet(vector GibLocation, rotator GibRotation, float MomentumScale, KFGibletInfo InGibInfo, vector InDamageOrigin, optional class<KFDamageType> InDamageType)
|
||
|
{
|
||
|
local KFGiblet Gib;
|
||
|
local bool bSpinGib;
|
||
|
local vector ImpluseDir, ModifiedImpulseDir;
|
||
|
|
||
|
if( WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.bDropDetail || WorldInfo.GetDetailMode() == DM_Low )
|
||
|
{
|
||
|
return None;
|
||
|
}
|
||
|
|
||
|
// Spawn gib at the location of the pawn
|
||
|
Gib = Spawn(class'KFGame.KFGiblet', self, , GibLocation, GibRotation);
|
||
|
Gib.SetMesh(InGibInfo);
|
||
|
|
||
|
if ( Gib != None )
|
||
|
{
|
||
|
// Take input impulse dir and modify it to face upwards -->
|
||
|
// Halfway between input and WorldUp. Divide by 2 is irrelevant for normal vector
|
||
|
ImpluseDir = Normal(Gib.Location - InDamageOrigin);
|
||
|
ModifiedImpulseDir = Normal(ImpluseDir + vect(0,0,1));
|
||
|
|
||
|
// START DEBUG
|
||
|
// DrawDebugLine(PawnLocation, PawnLocation + 200.f*ImpluseDir, 255, 0, 0, TRUE);
|
||
|
// DrawDebugLine(PawnLocation, PawnLocation + 200.f*ModifiedImpulseDir, 255, 255, 255, TRUE);
|
||
|
// END DEBUG
|
||
|
|
||
|
// add initial impulse
|
||
|
if ( InDamageType != None )
|
||
|
{
|
||
|
Gib.Velocity = ModifiedImpulseDir * FMin(InDamageType.default.RadialDamageImpulse, InGibInfo.GibletMaxSpeed);
|
||
|
}
|
||
|
|
||
|
// Randomize gib velocity +/- 10%;
|
||
|
Gib.Velocity = (Gib.Velocity + Gib.Velocity * 0.2) - (FRand() * Gib.Velocity * 0.2);
|
||
|
|
||
|
// Scale the velocity by the momentum scale from the explosion or damage
|
||
|
Gib.Velocity *= MomentumScale;
|
||
|
|
||
|
Gib.GibMeshComp.WakeRigidBody();
|
||
|
Gib.GibMeshComp.SetRBLinearVelocity(Gib.Velocity, false);
|
||
|
|
||
|
// Random chance to spin gibs
|
||
|
bSpinGib = FRand() > 0.5;
|
||
|
if ( bSpinGib )
|
||
|
{
|
||
|
Gib.GibMeshComp.SetRBAngularVelocity(VRand() * 500, false);
|
||
|
}
|
||
|
|
||
|
// Auto-destruct after sometime
|
||
|
Gib.SetTimer(GibletLifetime * GoreFXLifetimeMultiplier, false, 'LifespanTimer');
|
||
|
}
|
||
|
|
||
|
return Gib;
|
||
|
}
|
||
|
|
||
|
/** Obliteration effects */
|
||
|
simulated function SpawnObliterationBloodEffect(KFPawn InPawn)
|
||
|
{
|
||
|
local ParticleSystemComponent PSC;
|
||
|
local KFCHaracterInfo_Monster MonsterInfo;
|
||
|
local vector ParticleLocation;
|
||
|
|
||
|
// Play obliteration sound
|
||
|
InPawn.SoundGroupArch.PlayObliterationSound(InPawn);
|
||
|
|
||
|
MonsterInfo = KFCharacterInfo_Monster(InPawn.GetCharacterInfo());
|
||
|
// Particle location is the spine. If no spine, then it will be the pawn's location.
|
||
|
ParticleLocation = InPawn.Mesh.GetBoneLocation('Spine');
|
||
|
if( IsZero(ParticleLocation) )
|
||
|
{
|
||
|
ParticleLocation = InPawn.Location;
|
||
|
}
|
||
|
PSC = MiscGoreFXEmitterPool.SpawnEmitter(MonsterInfo.ObliterationEffectTemplate, ParticleLocation);
|
||
|
PSC.SetLightingChannels(InPawn.PawnLightingChannel);
|
||
|
}
|
||
|
|
||
|
/** @deprecated Obliteration effects */
|
||
|
simulated function CauseObliteration(KFPawn InPawn, vector InDamageOrigin, class<KFDamageType> InDamageType, optional float MomentumScale = 1.f)
|
||
|
{
|
||
|
local KFCharacterInfo_Monster MonsterInfo;
|
||
|
local int GibletIndex, BoneIndex;
|
||
|
local vector PawnLocation;
|
||
|
local KFPawnSoundGroup PawnSoundGroup;
|
||
|
local KFGiblet Gib;
|
||
|
local vector Loc;
|
||
|
local rotator Rot;
|
||
|
local Quat BoneQuat;
|
||
|
local bool bSpawnedAGibForThisIndex;
|
||
|
|
||
|
// Cache the pawn's location
|
||
|
PawnLocation = InPawn.Location;
|
||
|
PawnSoundGroup = InPawn.SoundGroupArch;
|
||
|
|
||
|
// Play obliteration sound
|
||
|
PawnSoundGroup.PlayObliterationSound(InPawn);
|
||
|
|
||
|
// Destroy the pawn
|
||
|
InPawn.Mesh.SetHidden(TRUE);
|
||
|
|
||
|
// Remove the pawn from the corse pool
|
||
|
// NOTE: By the time obliteration happens, the corpse has already been added to the corpse pool becase PlayDying is handled earler.
|
||
|
RemoveAndDeleteCorpse(CorpsePool.Find(InPawn));
|
||
|
|
||
|
MonsterInfo = KFCharacterInfo_Monster(InPawn.GetCharacterInfo());
|
||
|
if ( MonsterInfo != None )
|
||
|
{
|
||
|
SpawnObliterationBloodEffect(InPawn);
|
||
|
|
||
|
// Spawn giblets - limbs, soft bodies, etc.
|
||
|
for( GibletIndex = 0; GibletIndex < MonsterInfo.GibletSettings.length; GibletIndex++ )
|
||
|
{
|
||
|
bSpawnedAGibForThisIndex=false;
|
||
|
|
||
|
if( MonsterInfo.GibletSettings[GibletIndex].GibletBones.length > 0 )
|
||
|
{
|
||
|
for( BoneIndex = 0; BoneIndex < MonsterInfo.GibletSettings[GibletIndex].GibletBones.length; BoneIndex++ )
|
||
|
{
|
||
|
// Randomly skip some of the gibs so we don't always spawn the exact same set
|
||
|
if( MonsterInfo.GibletSettings[GibletIndex].GibletBones.length > 1 && (BoneIndex < (MonsterInfo.GibletSettings[GibletIndex].GibletBones.length - 1)
|
||
|
|| bSpawnedAGibForThisIndex ) )
|
||
|
{
|
||
|
if( FRand() < 0.35 )
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Find the location/roation of the bones where this gib should spawn at
|
||
|
Loc = InPawn.Mesh.GetBoneLocation(MonsterInfo.GibletSettings[GibletIndex].GibletBones[BoneIndex]);
|
||
|
BoneQuat = InPawn.Mesh.GetBoneQuaternion(MonsterInfo.GibletSettings[GibletIndex].GibletBones[BoneIndex]);
|
||
|
Rot = QuatToRotator(BoneQuat);
|
||
|
|
||
|
Gib = SpawnGiblet(Loc, Rot, MomentumScale, MonsterInfo.GibletSettings[GibletIndex], InDamageOrigin, InDamageType);
|
||
|
bSpawnedAGibForThisIndex=true;
|
||
|
if ( Gib != None )
|
||
|
{
|
||
|
Gib.SoundGroup = PawnSoundGroup;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Fallback to spawning at the pawn location if the info isn't set for the bones in the archetype
|
||
|
Loc = PawnLocation;
|
||
|
Gib = SpawnGiblet(Loc, Rot, MomentumScale, MonsterInfo.GibletSettings[GibletIndex], InDamageOrigin, InDamageType);
|
||
|
bSpawnedAGibForThisIndex=true;
|
||
|
if ( Gib != None )
|
||
|
{
|
||
|
Gib.SoundGroup = PawnSoundGroup;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************
|
||
|
* Ragdoll
|
||
|
*********************************************************************************************/
|
||
|
|
||
|
/** Remove at least one corpse from the pool to make room for a fresh body */
|
||
|
native function bool MakeRoomForCorpse(KFPawn InCorpse);
|
||
|
/** Rate a corpse priority based on last render time and other parameters */
|
||
|
native function float RateCorpse(KFPawn InCorpse);
|
||
|
/** Cleanup */
|
||
|
native function bool RemoveAndDeleteCorpse(int PoolIdx);
|
||
|
native function bool DeleteCorpse(KFPawn InCorpse);
|
||
|
|
||
|
/** Add pawn to the dead body pool */
|
||
|
simulated function AddCorpse(KFPawn NewCorpse)
|
||
|
{
|
||
|
// if the pool is full try to remove something first
|
||
|
if ( CorpsePool.Length >= MaxDeadBodies )
|
||
|
{
|
||
|
MakeRoomForCorpse(NewCorpse);
|
||
|
}
|
||
|
|
||
|
if ( CorpsePool.Length < MaxDeadBodies )
|
||
|
{
|
||
|
// If there is room, disable lifespan and add to the pool
|
||
|
NewCorpse.LifeSpan = 0.f;
|
||
|
CorpsePool.AddItem(NewCorpse);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// If there is still no room, delete the new corpse
|
||
|
DeleteCorpse(NewCorpse);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Partial Reset */
|
||
|
function ResetPersistantGore(optional bool bOnRespawn)
|
||
|
{
|
||
|
local int i;
|
||
|
|
||
|
if ( bOnRespawn )
|
||
|
{
|
||
|
// remove all humans from the corpse pool during respawn
|
||
|
for (i = CorpsePool.Length-1; i >= 0; i--)
|
||
|
{
|
||
|
if ( CorpsePool[i].IsA('KFPawn_Human') )
|
||
|
{
|
||
|
RemoveAndDeleteCorpse(i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Level was reset without reloading */
|
||
|
event Reset()
|
||
|
{
|
||
|
local int i;
|
||
|
|
||
|
for (i = 0; i < CorpsePool.Length; i++)
|
||
|
{
|
||
|
CorpsePool[i].Destroy();
|
||
|
}
|
||
|
CorpsePool.Remove(0, CorpsePool.Length);
|
||
|
|
||
|
ClearPersistentBloodSplats();
|
||
|
}
|
||
|
|
||
|
cpptext
|
||
|
{
|
||
|
virtual UBOOL Tick( FLOAT DeltaTime, enum ELevelTick TickType );
|
||
|
}
|
||
|
|
||
|
defaultproperties
|
||
|
{
|
||
|
TickGroup=TG_PreAsyncWork
|
||
|
// Actor
|
||
|
bTickIsDisabled=false
|
||
|
|
||
|
// Corpse Pool
|
||
|
MaxCorpseOffscreenTime=60.f
|
||
|
MaxCorpseOffscreenDistance=5000.f;
|
||
|
|
||
|
// Persistent splats
|
||
|
CurrentSplatIdx=0
|
||
|
|
||
|
// Debugging
|
||
|
bShowPersistentBloodTraces=false
|
||
|
}
|