//============================================================================= // 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 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 CurrentSplats; var transient int CurrentSplatIdx; var transient array 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 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 InDmgType, vector InHitLocation, vector InHitDirection, int InHitZoneIndex, bool bIsDismeberingHit, bool bWasObliterated) { local array 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 BloodJets, optional array BloodTrails, optional array 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 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 InDmgType, vector InHitLocation, vector InHitDirection, name InHitBoneName, optional bool bForceApply) { local int i, JointIndex, PartialBreakIndex, ClosestBoneIndex; local name ClosestBone; local array ClosestBoneList; local KFCharacterInfo_Monster MonsterInfo; local PartialBreakSettings CurrentPartialBreak; local array 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 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 InDmgType, vector InExplosionOrigin, array 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 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 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 }