1017 lines
37 KiB
Ucode
1017 lines
37 KiB
Ucode
//======================================================================================================================
|
|
// NicePack / NiceBullet
|
|
//======================================================================================================================
|
|
// Bullet class that's supposed to take care of all damage-dealing projectile needs.
|
|
// Functionality:
|
|
// - Simulation of both linear and piece-wise motion
|
|
// - Collision detection that can be handled through the adapter class
|
|
//======================================================================================================================
|
|
// 'Nice pack' source
|
|
// Do whatever the fuck you want with it
|
|
// Author: dkanus
|
|
// E-mail: dkanus@gmail.com
|
|
//======================================================================================================================
|
|
class NiceBullet extends Actor;
|
|
|
|
// Link to interaction with the server
|
|
var NiceReplicationInfo niceRI;
|
|
// Controller of our instigator
|
|
var NicePlayerController nicePlayer;
|
|
// Controller of local player
|
|
var NicePlayerController localPlayer;
|
|
// Link to our mutator
|
|
var NicePack niceMutator;
|
|
//======================================================================================================================
|
|
// Battle bullet characteristic
|
|
var float charOrigDamage;
|
|
var float charDamage;
|
|
var float decapMod;
|
|
var float incapMod;
|
|
var int charPenetrationCount;
|
|
var float charMomentumTransfer;
|
|
var class<NiceWeaponDamageType> charDamageType;
|
|
var class<NiceWeaponDamageType> charExplosionDamageType;
|
|
var float charExplosionDamage;
|
|
var float charExplosionRadius;
|
|
var float charExplosionExponent;
|
|
var float charExplosionMomentum;
|
|
var bool charIsDud;
|
|
var float charFuseTime;
|
|
var bool charExplodeOnFuse;
|
|
var bool charExplodeOnPawnHit;
|
|
var bool charExplodeOnWallHit;
|
|
var bool charAffectedByScream;
|
|
var float charMinExplosionDist;
|
|
var bool charWasHipFired;
|
|
var bool charCausePain;
|
|
var bool charIsSticky;
|
|
var float charContiniousBonus;
|
|
var bool bAlreadyHitZed;
|
|
var NiceWeapon sourceWeapon;
|
|
var NiceMonster lockonZed;
|
|
var float lockonTime;
|
|
var float bounceHeadMod;
|
|
var int insideBouncesLeft;
|
|
var bool bGrazing;
|
|
|
|
//======================================================================================================================
|
|
// Sticky projectile-related variables
|
|
var bool bStuck, bUseBone, bStuckToHead;
|
|
var name stuckBone;
|
|
var int stuckID;
|
|
// Data about explosive characteristics that must be transfered after projectile sticks on something
|
|
struct ExplosionData{
|
|
var Pawn instigator;
|
|
var NiceWeapon sourceWeapon;
|
|
var class<NiceBullet> bulletClass;
|
|
var class<NiceWeaponDamageType> explosionDamageType;
|
|
var float explosionDamage;
|
|
var float explosionRadius;
|
|
var float explosionExponent;
|
|
var float explosionMomentum;
|
|
var float fuseTime;
|
|
var bool explodeOnFuse;
|
|
var bool affectedByScream;
|
|
var bool stuckToHead;
|
|
};
|
|
|
|
//======================================================================================================================
|
|
// Temporary variables that either need to be moved into config or generalized later
|
|
// How often trajectory is allowed to change direction?
|
|
var const float trajUpdFreq;
|
|
// How much tracing cycles to endure before declaring an infinite cycle?
|
|
var const int maxTraceCycles;
|
|
var bool bCanAngleDamage;
|
|
|
|
//======================================================================================================================
|
|
// State variables
|
|
// Indicates that all necessary values were recorded and we can process this bullet normally
|
|
var bool bInitFinished;
|
|
var bool bInitFinishDetected;
|
|
// Disables all the interaction of this bullet with the world and removes it / marks it for removal
|
|
var bool bBulletDead;
|
|
var bool bGhost;
|
|
|
|
//======================================================================================================================
|
|
// Collision management
|
|
// Describes an actor that we don't wish to collide with
|
|
struct IgnoreEntry{
|
|
var Actor ignored;
|
|
// Set to true when 3rd party already disabled this actor before we had the chance
|
|
// Used to avoid re-enabling it later
|
|
var bool bExtDisabled;
|
|
};
|
|
var array<IgnoreEntry> ignoredActors;
|
|
// 'true' if we're enforcing our collision rules on actors to ignore them
|
|
var bool bIgnoreIsActive;
|
|
|
|
//======================================================================================================================
|
|
// Movement
|
|
// If 'true' disables any modifications to the bullet's movement, making it travel in a simple, linear path
|
|
var bool bDisableComplexMovement;
|
|
// Linear motion
|
|
var float movementSpeed;
|
|
var Vector movementDirection;
|
|
// Added vector acceleration
|
|
var Vector movementAcceleration;
|
|
var bool bShouldBounce;
|
|
// Amount of time our bullet should fall down after hitting a wall
|
|
var float movementFallTime;
|
|
|
|
//======================================================================================================================
|
|
// Path building
|
|
// We will be building a piecewise linear path for projectiles, where each linear segment should be passable by
|
|
// projectile in time 'trajUpdFreq'.
|
|
// By changing trajectory in only preset point allow client to emulate non-linear paths,
|
|
// while keeping them more or less independent from the frame rate.
|
|
// Start and End point of the current linear segment
|
|
var Vector pathSegmentS, pathSegmentE;
|
|
// Point in the segment, at which we've stopped after last movement
|
|
var Vector shiftPoint;
|
|
// The part of current segment that we've already covered, changes from 0.0 to 1.0.
|
|
// Values above 1.0 indicate that segment was finished and we need to build another one.
|
|
// Values below 0.0 indicate that no segment has yet been built.
|
|
var float finishedSegmentPart;
|
|
|
|
//======================================================================================================================
|
|
// Visual effects
|
|
var class<Emitter> trailClass;
|
|
var class<xEmitter> trailXClass;
|
|
var Emitter bulletTrail;
|
|
var xEmitter bulletXTrail;
|
|
// Describes effect that projectile should produce on hit
|
|
struct ImpactEffect {
|
|
// Is this effect too important to cut it due to effect limit?
|
|
var bool bImportanEffect;
|
|
// Should we play classic KF's hit effect ('ROBulletHitEffect')?
|
|
var bool bPlayROEffect;
|
|
// Decal to spawn; null to skip
|
|
var class<Projector> decalClass;
|
|
// Emitter to spawn; null to skip
|
|
var class<Emitter> emitterClass;
|
|
// How much back (against direction of the shot) should we spawn emitter? Can be used to avoid clipping with walls
|
|
var float emitterShiftWall; // Shift for wall-hits
|
|
var float emitterShiftPawn; // Shift for pawn-hits
|
|
// Impact noise parameters
|
|
var Sound noise;
|
|
var string noiseRef; // Reference to 'Sound' to allow dynamic resource allocation
|
|
var float noiseVolume;
|
|
};
|
|
var ImpactEffect regularImpact;
|
|
var ImpactEffect explosionImpact;
|
|
var ImpactEffect disintegrationImpact;
|
|
var bool bGenRegEffectOnPawn;
|
|
var bool bShakeViewOnExplosion;
|
|
var Vector shakeRotMag; // how far to rot view
|
|
var Vector shakeRotRate; // how fast to rot view
|
|
var float shakeRotTime; // how much time to rot the instigator's view
|
|
var Vector shakeOffsetMag; // max view offset vertically
|
|
var Vector shakeOffsetRate; // how fast to offset view vertically
|
|
var float shakeOffsetTime; // how much time to offset view
|
|
var float shakeRadiusMult;
|
|
|
|
//======================================================================================================================
|
|
// References
|
|
// References to allow pre-loading of some resource objects, declared in parent classes
|
|
var string meshRef;
|
|
var string staticMeshRef;
|
|
var string ambientSoundRef;
|
|
|
|
//======================================================================================================================
|
|
// Functions
|
|
static function PreloadAssets() {
|
|
if (default.ambientSound == none && default.ambientSoundRef != "") {
|
|
default.ambientSound = sound(DynamicLoadObject(default.ambientSoundRef, class'Sound', true));
|
|
}
|
|
if (default.staticMesh == none && default.staticMeshRef != "") {
|
|
UpdateDefaultStaticMesh(StaticMesh(DynamicLoadObject(default.staticMeshRef, class'StaticMesh', true)));
|
|
}
|
|
if (default.mesh == none && default.meshRef != "") {
|
|
UpdateDefaultMesh(Mesh(DynamicLoadObject(default.meshRef, class'Mesh', true)));
|
|
}
|
|
if (default.regularImpact.noise == none && default.regularImpact.noiseRef != "") {
|
|
default.regularImpact.noise =
|
|
sound(DynamicLoadObject(default.regularImpact.noiseRef, class'Sound', true));
|
|
}
|
|
if (default.explosionImpact.noise == none && default.explosionImpact.noiseRef != "") {
|
|
default.explosionImpact.noise =
|
|
sound(DynamicLoadObject(default.explosionImpact.noiseRef, class'Sound', true));
|
|
}
|
|
if (default.disintegrationImpact.noise == none && default.disintegrationImpact.noiseRef != "") {
|
|
default.disintegrationImpact.noise =
|
|
sound(DynamicLoadObject(default.disintegrationImpact.noiseRef, class'Sound', true));
|
|
}
|
|
}
|
|
|
|
static function bool UnloadAssets() {
|
|
default.AmbientSound = none;
|
|
UpdateDefaultStaticMesh(none);
|
|
UpdateDefaultMesh(none);
|
|
default.regularImpact.noise = none;
|
|
default.explosionImpact.noise = none;
|
|
default.disintegrationImpact.noise = none;
|
|
return true;
|
|
}
|
|
|
|
function PostBeginPlay() {
|
|
super.PostBeginPlay();
|
|
bounceHeadMod = 1.0;
|
|
}
|
|
|
|
function UpdateTrails() {
|
|
local Actor trailBase;
|
|
|
|
// Do nothing on dedicated server
|
|
if (Level.NetMode == NM_DedicatedServer) {
|
|
return;
|
|
}
|
|
// Spawn necessary trails first
|
|
if (trailClass != none && bulletTrail == none) {
|
|
bulletTrail = Spawn(trailClass, self);
|
|
}
|
|
if (trailXClass != none && bulletXTrail == none) {
|
|
bulletXTrail = Spawn(trailXClass, self);
|
|
}
|
|
// Handle positioning differently for stuck and regular projectiles
|
|
if (bStuck && base != none) {
|
|
if (bUseBone) {
|
|
if (bulletTrail != none) {
|
|
bulletTrail.SetBase(base);
|
|
base.AttachToBone(bulletTrail, stuckBone);
|
|
bulletTrail.SetRelativeLocation(relativeLocation);
|
|
bulletTrail.SetRelativeRotation(relativeRotation);
|
|
}
|
|
if (bulletXTrail != none) {
|
|
bulletXTrail.SetBase(base);
|
|
base.AttachToBone(bulletTrail, stuckBone);
|
|
bulletXTrail.SetRelativeLocation(relativeLocation);
|
|
bulletXTrail.SetRelativeRotation(relativeRotation);
|
|
}
|
|
}
|
|
} else {
|
|
trailBase = self;
|
|
}
|
|
|
|
// Update lifetime and base (latter is for non-bone attachments only)
|
|
if (bulletTrail != none) {
|
|
if (trailBase != none) {
|
|
bulletTrail.SetBase(trailBase);
|
|
}
|
|
bulletTrail.lifespan = lifeSpan;
|
|
}
|
|
if (bulletXTrail != none) {
|
|
if (trailBase != none) {
|
|
bulletXTrail.SetBase(trailBase);
|
|
}
|
|
bulletXTrail.lifespan = lifeSpan;
|
|
}
|
|
}
|
|
|
|
function ResetPathBuilding() {
|
|
finishedSegmentPart = -1.0;
|
|
shiftPoint = location;
|
|
}
|
|
|
|
// Resets default values for this bullet.
|
|
// Must be called before each new use of a bullet.
|
|
function Renew() {
|
|
bInitFinished = false;
|
|
bBulletDead = false;
|
|
bCanAngleDamage = true;
|
|
SoundVolume = default.SoundVolume;
|
|
decapMod = 1.0f;
|
|
incapMod = 1.0f;
|
|
ResetIgnoreList();
|
|
ResetPathBuilding();
|
|
}
|
|
|
|
simulated function Tick(float delta) {
|
|
super.Tick(delta);
|
|
|
|
if (localPlayer == none) {
|
|
localPlayer = NicePlayerController(Level.GetLocalPlayerController());
|
|
}
|
|
|
|
if (charFuseTime > 0) {
|
|
charFuseTime -= delta;
|
|
if (charFuseTime < 0) {
|
|
if (charExplodeOnFuse && !charIsDud) {
|
|
GenerateImpactEffects(explosionImpact, location, movementDirection);
|
|
if (bStuck) {
|
|
class'NiceBulletAdapter'.static.Explode(self, niceRI, location, base);
|
|
} else {
|
|
class'NiceBulletAdapter'.static.Explode(self, niceRI, location);
|
|
}
|
|
}
|
|
if (!charExplodeOnFuse) {
|
|
GenerateImpactEffects(disintegrationImpact, location, movementDirection);
|
|
}
|
|
KillBullet();
|
|
}
|
|
}
|
|
|
|
if (bInitFinished && !bInitFinishDetected) {
|
|
bInitFinishDetected = true;
|
|
UpdateTrails();
|
|
}
|
|
if (bInitFinished && !bBulletDead && !bStuck) {
|
|
DoProcessMovement(delta);
|
|
}
|
|
if (bInitFinished && bStuck) {
|
|
if (base == none || (KFMonster(base) != none && KFMonster(base).health <= 0)) {
|
|
nicePlayer.ExplodeStuckBullet(stuckID);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extracts pawn actor from it's auxiliary collision
|
|
// @param other Actor we collided with
|
|
// @return Pawn we're interested in
|
|
function Actor GetMainActor(Actor other) {
|
|
if (other == none) {
|
|
return none;
|
|
}
|
|
|
|
if (!other.IsA('KFPawn') && !other.IsA('KFMonster')) {
|
|
// Try owner
|
|
if (other.owner.IsA('KFPawn') || other.owner.IsA('KFMonster')) {
|
|
return other.owner;
|
|
}
|
|
// Try base
|
|
if (other.base.IsA('KFPawn') || other.base.IsA('KFMonster')) {
|
|
return other.base;
|
|
}
|
|
}
|
|
|
|
return other;
|
|
}
|
|
|
|
// Returns 'true' if passed actor is either world geometry, 'Level' itself or nothing ('none')
|
|
// Neither of these related to pawn damage dealing
|
|
function bool IsLevelActor(Actor other) {
|
|
if (other == none) {
|
|
return true;
|
|
}
|
|
return (other.bWorldGeometry || other == Level);
|
|
}
|
|
|
|
// Adds given actor and every colliding object connected to it to ignore list
|
|
// Removes their collision in case ignore is active (see 'bIgnoreIsActive')
|
|
function TotalIgnore(Actor other) {
|
|
// These mark what objects, associated with 'other' we also need to ignore
|
|
local KFPawn pawnOther;
|
|
local KFMonster zedOther;
|
|
|
|
if (other == none) {
|
|
return;
|
|
}
|
|
// Try to find main actor as KFPawn
|
|
pawnOther = KFPawn(other);
|
|
if (pawnOther == none) {
|
|
pawnOther = KFPawn(other.base);
|
|
}
|
|
if (pawnOther == none) {
|
|
pawnOther = KFPawn(other.owner);
|
|
}
|
|
// Try to find main actor as KFMonster
|
|
zedOther = KFMonster(other);
|
|
if (zedOther == none) {
|
|
zedOther = KFMonster(other.base);
|
|
}
|
|
if (zedOther == none) {
|
|
zedOther = KFMonster(other.owner);
|
|
}
|
|
// Ignore everything that's associated with this actor and can have collision
|
|
IgnoreActor(other);
|
|
IgnoreActor(other.base);
|
|
IgnoreActor(other.owner);
|
|
if (pawnOther != none) {
|
|
IgnoreActor(pawnOther.AuxCollisionCylinder);
|
|
}
|
|
if (zedOther != none) {
|
|
IgnoreActor(zedOther.MyExtCollision);
|
|
}
|
|
}
|
|
|
|
// Adds a given actor to ignore list and removes it's collision in case ignore is active (see 'bIgnoreIsActive')
|
|
function IgnoreActor(Actor other) {
|
|
local int i;
|
|
local IgnoreEntry newIgnoredEntry;
|
|
|
|
// Check if that's a non-level actor and not already on the list
|
|
if (IsLevelActor(other)) {
|
|
return;
|
|
}
|
|
for (i = 0; i < ignoredActors.Length; i ++) {
|
|
if (ignoredActors[i].ignored == other) {
|
|
return;
|
|
}
|
|
}
|
|
// Add actor to the ignore list & disable collision if needed
|
|
if (other != none) {
|
|
// Make entry
|
|
newIgnoredEntry.ignored = other;
|
|
newIgnoredEntry.bExtDisabled = !other.bCollideActors;
|
|
// Add and activate it
|
|
ignoredActors[ignoredActors.Length] = newIgnoredEntry;
|
|
if (bIgnoreIsActive) {
|
|
other.SetCollision(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restores ignored state of the actors and zeroes our ignored arrays
|
|
function ResetIgnoreList() {
|
|
SetIgnoreActive(false);
|
|
ignoredActors.Length = 0;
|
|
}
|
|
|
|
// Activates/deactivates ignore for actors on the ignore list.
|
|
// Ignore deactivation doesn't restore collision if actor was set to not collide prior most recent ignore activation.
|
|
// Activating ignore when it's already active does nothing; same with deactivation.
|
|
// Ignore deactivation is supposed to be used in the same function call in which activation took place before.
|
|
function SetIgnoreActive(bool bActive) {
|
|
local int i;
|
|
|
|
// Do nothing if we're already in a correct state
|
|
if (bActive == bIgnoreIsActive) {
|
|
return;
|
|
}
|
|
// Change ignore state & disable collision for ignored actors
|
|
bIgnoreIsActive = bActive;
|
|
for (i = 0; i < ignoredActors.Length; i ++) {
|
|
if (ignoredActors[i].ignored != none) {
|
|
// Mark actors that were set to not collide before activation
|
|
if (bActive && !ignoredActors[i].ignored.bCollideActors) {
|
|
ignoredActors[i].bExtDisabled = true;
|
|
}
|
|
// Change collision for actors that weren't externally modified
|
|
if (!ignoredActors[i].bExtDisabled) {
|
|
ignoredActors[i].ignored.SetCollision(!bActive);
|
|
}
|
|
// After we deactivated our rules - forget about external modifications
|
|
if (!bActive) {
|
|
ignoredActors[i].bExtDisabled = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function SetHumanPawnCollision(bool bEnable) {
|
|
local int i;
|
|
|
|
if (niceMutator == none) {
|
|
niceMutator = class'NicePack'.static.Myself(Level);
|
|
}
|
|
|
|
for (i = 0; i < niceMutator.recordedHumanPawns.Length; i ++) {
|
|
if (niceMutator.recordedHumanPawns[i] != none) {
|
|
niceMutator.recordedHumanPawns[i].bBlockHitPointTraces = bEnable;
|
|
}
|
|
}
|
|
}
|
|
|
|
function float CheckHeadshot(KFMonster kfZed, Vector hitLocation, Vector hitDirection) {
|
|
local float hsMod;
|
|
local float precision;
|
|
local bool bIsShotgunBullet;
|
|
local NiceMonster niceZed;
|
|
local KFPlayerReplicationInfo KFPRI;
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
|
|
niceZed = NiceMonster(kfZed);
|
|
hitDirection = Normal(hitDirection);
|
|
bIsShotgunBullet = ClassIsChildOf(charDamageType, class'NiceDamageTypeVetEnforcer');
|
|
if (niceZed != none) {
|
|
hsMod = bounceHeadMod; // NICETODO: Add bounce and perk and damage type head-shot zones bonuses
|
|
hsMod *= charDamageType.default.headSizeModifier;
|
|
hsMod *= bounceHeadMod;
|
|
if (nicePlayer != none) {
|
|
KFPRI = KFPlayerReplicationInfo(nicePlayer.PlayerReplicationInfo);
|
|
}
|
|
if (KFPRI != none) {
|
|
niceVet = class<NiceVeterancyTypes>(KFPRI.ClientVeteranSkill);
|
|
}
|
|
if (niceVet != none) {
|
|
hsMod *= niceVet.static.GetHeadshotCheckMultiplier(KFPRI, charDamageType);
|
|
}
|
|
precision = niceZed.IsHeadshotClient(hitLocation, hitDirection, niceZed.clientHeadshotScale * hsMod);
|
|
if (
|
|
precision <= 0.0 &&
|
|
bIsShotgunBullet &&
|
|
nicePlayer != none &&
|
|
class'NiceVeterancyTypes'.static.hasSkill(nicePlayer, class'NiceSkillSupportGraze')
|
|
) {
|
|
bGrazing = true;
|
|
hsMod *= class'NiceSkillSupportGraze'.default.hsBonusZoneMult;
|
|
precision = niceZed.IsHeadshotClient(hitLocation, hitDirection, niceZed.clientHeadshotScale * hsMod);
|
|
}
|
|
return precision;
|
|
} else {
|
|
if (kfZed.IsHeadShot(hitLocation, hitDirection, 1.0)) {
|
|
return 1.0;
|
|
} else {
|
|
return 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Makes bullet trace a directed line segment given by start and end points.
|
|
// All traced actors and geometry are then properly affected by corresponding 'HandleHitPawn', 'HandleHitZed' and
|
|
// 'HandleHitWall' functions.
|
|
// Might have to do several traces in case it either hits a (several) target(s)
|
|
function DoTraceLine(Vector lineStart, Vector lineEnd) {
|
|
local float headshotLevel;
|
|
// Amount of tracing iterations we had to do
|
|
local int iterationCount;
|
|
// Direction and length of traced line
|
|
local Vector lineDirection;
|
|
// Auxiliary variables for retrieving results of tracing
|
|
local Vector hitLocation, hitNormal;
|
|
local Actor tracedActor;
|
|
local array<int> hitPoints;
|
|
local KFMonster tracedZed;
|
|
local KFPawn tracedPawn;
|
|
|
|
lineDirection = (lineEnd - lineStart);
|
|
lineDirection = (lineDirection) / VSize(lineDirection);
|
|
// Do not trace for disabled bullets and prevent infinite loops
|
|
while (!bBulletDead && iterationCount < maxTraceCycles) {
|
|
iterationCount++;
|
|
// Trace next object
|
|
if (!bGhost || localPlayer == none || localPlayer.tracesThisTick <= localPlayer.tracesPerTickLimit) {
|
|
if (Instigator != none) {
|
|
tracedActor = Instigator.Trace(hitLocation, hitNormal, lineEnd, lineStart, true);
|
|
} else {
|
|
tracedActor = none;
|
|
}
|
|
localPlayer.tracesThisTick++;
|
|
} else {
|
|
tracedActor = none;
|
|
}
|
|
if (
|
|
charAffectedByScream &&
|
|
!charIsDud &&
|
|
localPlayer != none &&
|
|
localPlayer.localCollisionManager != none &&
|
|
localPlayer.localCollisionManager.IsCollidingWithAnything(lineStart, lineEnd)
|
|
) {
|
|
HandleScream(lineStart, lineDirection);
|
|
}
|
|
if (tracedActor != none && IsLevelActor(tracedActor)) {
|
|
HandleHitWall(tracedActor, hitLocation, hitNormal);
|
|
break;
|
|
} else {
|
|
TotalIgnore(tracedActor);
|
|
tracedActor = GetMainActor(tracedActor);
|
|
}
|
|
|
|
// If tracing between current trace points haven't found anything and tracing step is less than segment's length
|
|
// -- shift tracing bounds
|
|
if (tracedActor == none) {
|
|
return;
|
|
}
|
|
|
|
// First, try to handle pawn like a zed; if fails, - try to handle it like 'KFPawn'
|
|
tracedZed = KFMonster(tracedActor);
|
|
tracedPawn = KFPawn(tracedActor);
|
|
if (
|
|
tracedPawn != none &&
|
|
NiceHumanPawn(instigator) != none &&
|
|
(NiceHumanPawn(instigator).ffScale <= 0 && NiceMedicProjectile(self) == none)
|
|
) {
|
|
continue;
|
|
}
|
|
if (tracedZed != none) {
|
|
if (tracedZed.Health > 0) {
|
|
headshotLevel = CheckHeadshot(tracedZed, hitLocation, lineDirection);
|
|
HandleHitZed(tracedZed, hitLocation, lineDirection, headshotLevel);
|
|
}
|
|
} else if (tracedPawn != none && tracedPawn.Health > 0) {
|
|
if (tracedPawn.Health > 0) {
|
|
HandleHitPawn(tracedPawn, hitLocation, lineDirection, hitPoints);
|
|
}
|
|
} else {
|
|
HandleHitWall(tracedActor, hitLocation, hitNormal);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replaces current path segment with the next one.
|
|
// Doesn't check whether or not we've finished with the current segment.
|
|
function BuildNextPathSegment() {
|
|
// Only set start point to our location when we build path segment for the first time
|
|
// After that we can't even assume that bullet is exactly in the 'pathSegmentE' point
|
|
if (finishedSegmentPart < 0.0) {
|
|
pathSegmentS = Location;
|
|
} else {
|
|
pathSegmentS = pathSegmentE;
|
|
}
|
|
movementDirection += (movementAcceleration * trajUpdFreq) / movementSpeed;
|
|
pathSegmentE = pathSegmentS + movementDirection * movementSpeed * trajUpdFreq;
|
|
finishedSegmentPart = 0.0;
|
|
shiftPoint = pathSegmentS;
|
|
}
|
|
|
|
// Updates 'shiftPoint' to the next bullet position in current segment.
|
|
// Does nothing if current segment is finished or no segment was built at all.
|
|
// @param delta Amount of time bullet has to move through the segment.
|
|
// @return Amount of time left for bullet to move after this segment
|
|
function float ShiftInSegment(float delta) {
|
|
// Time that bullet still has available to move after this segment
|
|
local float remainingTime;
|
|
// Part of segment we can pass in a given time
|
|
local float segmentPartWeCanPass;
|
|
|
|
// Exit if there's no segment in progress
|
|
if (finishedSegmentPart < 0.0 || finishedSegmentPart > 1.0) {
|
|
return delta;
|
|
}
|
|
// [movementSpeed * delta] / [movementSpeed * trajUpdFreq] = [delta / trajUpdFreq]
|
|
segmentPartWeCanPass = delta / trajUpdFreq;
|
|
// If we can move through the rest of the segment - move to end point and mark it finished
|
|
if (segmentPartWeCanPass >= (1.0 - finishedSegmentPart)) {
|
|
remainingTime = delta - (1.0 - finishedSegmentPart) * trajUpdFreq;
|
|
finishedSegmentPart = 1.1;
|
|
shiftPoint = pathSegmentE;
|
|
} else {
|
|
// Otherwise compute new 'shiftPoint' normally
|
|
remainingTime = 0.0;
|
|
finishedSegmentPart += (delta / trajUpdFreq);
|
|
shiftPoint = pathSegmentS + movementDirection * movementSpeed * trajUpdFreq * finishedSegmentPart;
|
|
}
|
|
return remainingTime;
|
|
}
|
|
|
|
// Moves bullet according to settings and decides when and how much tracing should it do.
|
|
// @param delta Amount of time passed after previous bullet movement
|
|
function DoProcessMovement(float delta) {
|
|
local Vector tempVect;
|
|
|
|
SetIgnoreActive(true);
|
|
//SetHumanPawnCollision(true);
|
|
// Simple linear movement
|
|
if (bDisableComplexMovement) {
|
|
// Use 'traceStart' as a shift variable here
|
|
// Naming is bad in this case, but it avoids
|
|
tempVect = movementDirection * movementSpeed * delta;
|
|
DoTraceLine(location, location + tempVect);
|
|
Move(tempVect);
|
|
// Reset path building
|
|
// If in future complex movement would be re-enabled, - we want to set first point of the path to
|
|
// the location of bullet at a time and not use outdated information.
|
|
finishedSegmentPart = -1.0;
|
|
} else {
|
|
// Non-linear movement support
|
|
while(delta > 0.0) {
|
|
if (finishedSegmentPart < 0.0 || finishedSegmentPart > 1.0) {
|
|
BuildNextPathSegment();
|
|
}
|
|
// Remember current 'shiftPoint'. That's where we stopped tracing last time and where we must resume.
|
|
tempVect = shiftPoint;
|
|
// Update 'shiftPoint' (bullet position)
|
|
// and update how much time we've got left after we wasted some to move.
|
|
delta = ShiftInSegment(delta);
|
|
// Trace between end point of previous tracing and end point of the new one.
|
|
DoTraceLine(tempVect, shiftPoint);
|
|
}
|
|
tempVect = shiftPoint - location;
|
|
Move(shiftPoint - location);
|
|
}
|
|
SetRotation(Rotator(movementDirection));
|
|
if (charMinExplosionDist > 0) {
|
|
charMinExplosionDist -= VSize(tempVect);
|
|
}
|
|
SetIgnoreActive(false);
|
|
// SetHumanPawnCollision(false);
|
|
}
|
|
|
|
function Stick(Actor target, Vector hitLocation) {
|
|
local Vector boneStrickOrig;
|
|
local ExplosionData expData;
|
|
local Actor resultTarget;
|
|
local NiceMonster targetZed;
|
|
local name boneStick;
|
|
local float distToBone, t;
|
|
|
|
if (bGhost) {
|
|
return;
|
|
}
|
|
expData.instigator = instigator;
|
|
expData.sourceWeapon = sourceWeapon;
|
|
expData.bulletClass = class;
|
|
expData.explosionDamageType = charExplosionDamageType;
|
|
expData.explosionDamage = charExplosionDamage;
|
|
expData.explosionRadius = charExplosionRadius;
|
|
expData.explosionExponent = charExplosionExponent;
|
|
expData.explosionMomentum = charExplosionMomentum;
|
|
expData.fuseTime = charFuseTime;
|
|
expData.explodeOnFuse = charExplodeOnFuse;
|
|
expData.affectedByScream = charAffectedByScream;
|
|
|
|
if (!target.IsA('NiceMonster')) {
|
|
hitLocation -= target.location;
|
|
boneStick = 'None';
|
|
resultTarget = target;
|
|
} else {
|
|
targetZed = NiceMonster(target);
|
|
boneStick = targetZed.GetClosestBone(hitLocation, movementDirection, distToBone);
|
|
if (CheckHeadshot(targetZed, hitLocation, movementDirection) > 0.0) {
|
|
boneStick = targetZed.HeadBone;
|
|
}
|
|
if (boneStick == targetZed.HeadBone) {
|
|
expData.stuckToHead = true;
|
|
}
|
|
boneStrickOrig = targetZed.GetBoneCoords(boneStick).origin;
|
|
t = movementDirection.x * (boneStrickOrig.x - hitLocation.x) +
|
|
movementDirection.y * (boneStrickOrig.y - hitLocation.y) +
|
|
movementDirection.z * (boneStrickOrig.z - hitLocation.z);
|
|
t /= VSizeSquared(movementDirection);
|
|
t *= 0.5;
|
|
hitLocation = hitLocation + t * movementDirection;
|
|
hitLocation -= boneStrickOrig;
|
|
resultTarget = targetZed;
|
|
}
|
|
|
|
niceRI.ServerStickProjectile(
|
|
KFHumanPawn(instigator),
|
|
resultTarget,
|
|
boneStick,
|
|
hitLocation,
|
|
Rotator(movementDirection),
|
|
expData
|
|
);
|
|
class'NiceProjectileSpawner'.static.StickProjectile(
|
|
KFHumanPawn(instigator),
|
|
resultTarget,
|
|
boneStick,
|
|
hitLocation,
|
|
Rotator(movementDirection),
|
|
expData
|
|
);
|
|
|
|
KillBullet();
|
|
}
|
|
|
|
function DoExplode(Vector explLocation, Vector impactNormal) {
|
|
if (charIsDud) {
|
|
return;
|
|
}
|
|
if (bStuck) {
|
|
class'NiceBulletAdapter'.static.Explode(self, niceRI, explLocation, base);
|
|
} else {
|
|
class'NiceBulletAdapter'.static.Explode(self, niceRI, explLocation);
|
|
}
|
|
GenerateImpactEffects(explosionImpact, explLocation, impactNormal, true, true);
|
|
if (bShakeViewOnExplosion) {
|
|
ShakeView(explLocation);
|
|
}
|
|
KillBullet();
|
|
}
|
|
|
|
function HandleHitWall(Actor wall, Vector hitLocation, Vector hitNormal) {
|
|
local bool bBulletTooWeak;
|
|
|
|
if (charExplodeOnWallHit && !charIsDud && charMinExplosionDist <= 0.0) {
|
|
DoExplode(hitLocation, hitNormal);
|
|
return;
|
|
} else {
|
|
class'NiceBulletAdapter'.static.HitWall(self, niceRI, wall, hitLocation, hitNormal);
|
|
GenerateImpactEffects(regularImpact, hitLocation, hitNormal, true, true);
|
|
if (charIsSticky) {
|
|
Stick(wall, hitLocation);
|
|
}
|
|
}
|
|
|
|
if (bShouldBounce && !bDisableComplexMovement) {
|
|
movementDirection = (movementDirection - 2.0 * hitNormal * (movementDirection dot hitNormal));
|
|
bBulletTooWeak = !class'NiceBulletAdapter'.static.ZedPenetration(charDamage, self, none, false, false);
|
|
charPenetrationCount += 1;
|
|
ResetPathBuilding();
|
|
ResetIgnoreList();
|
|
bounceHeadMod *= 2;
|
|
} else if (movementFallTime > 0.0) {
|
|
charIsDud = true;
|
|
lifeSpan = movementFallTime;
|
|
movementFallTime = 0.0;
|
|
movementDirection = vect(0, 0, 0);
|
|
ResetPathBuilding();
|
|
ResetIgnoreList();
|
|
} else {
|
|
bBulletTooWeak = true;
|
|
}
|
|
|
|
if (bBulletTooWeak) {
|
|
KillBullet();
|
|
}
|
|
}
|
|
|
|
function HandleHitPawn(KFPawn hitPawn, Vector hitLocation, Vector hitDirection, array<int> hitPoints) {
|
|
if (charExplodeOnPawnHit && !charIsDud && charMinExplosionDist <= 0.0) {
|
|
DoExplode(hitLocation, hitDirection);
|
|
GenerateImpactEffects(explosionImpact, hitLocation, hitDirection);
|
|
return;
|
|
} else {
|
|
class'NiceBulletAdapter'.static.HitPawn(self, niceRI, hitPawn, hitLocation, hitDirection, hitPoints);
|
|
if (bGenRegEffectOnPawn) {
|
|
GenerateImpactEffects(regularImpact, hitLocation, hitDirection, false, false);
|
|
}
|
|
}
|
|
if (!class'NiceBulletAdapter'.static.ZedPenetration(charDamage, self, none, false, false)) {
|
|
charPenetrationCount += 1;
|
|
KillBullet();
|
|
}
|
|
}
|
|
|
|
function HandleHitZed(KFMonster targetZed, Vector hitLocation, Vector hitDirection, float headshotLevel) {
|
|
local bool bHitZedCalled;
|
|
|
|
if (class'NiceVeterancyTypes'.static.hasSkill(nicePlayer, class'NiceSkillDemoDirectApproach')) {
|
|
class'NiceBulletAdapter'.static.HitZed(self, niceRI, targetZed, hitLocation, hitDirection, headshotLevel);
|
|
if (bGenRegEffectOnPawn) {
|
|
GenerateImpactEffects(regularImpact, hitLocation, hitDirection, false, false);
|
|
}
|
|
bHitZedCalled = true;
|
|
}
|
|
|
|
if (charExplodeOnPawnHit && !charIsDud && charMinExplosionDist <= 0.0) {
|
|
class'NiceBulletAdapter'.static.Explode(self, niceRI, hitLocation, targetZed);
|
|
GenerateImpactEffects(explosionImpact, hitLocation, hitDirection);
|
|
if (bShakeViewOnExplosion) {
|
|
ShakeView(hitLocation);
|
|
}
|
|
KillBullet();
|
|
return;
|
|
} else {
|
|
if (!bHitZedCalled) {
|
|
class'NiceBulletAdapter'.static.HitZed(
|
|
self,
|
|
niceRI,
|
|
targetZed,
|
|
hitLocation,
|
|
hitDirection,
|
|
headshotLevel
|
|
);
|
|
if (bGenRegEffectOnPawn) {
|
|
GenerateImpactEffects(regularImpact, hitLocation, hitDirection, false, false);
|
|
}
|
|
}
|
|
bHitZedCalled = true;
|
|
if (!bGhost && !bAlreadyHitZed) {
|
|
bAlreadyHitZed = true;
|
|
if (nicePlayer != none && niceRI != none) {
|
|
niceRI.ServerJunkieExtension(nicePlayer, headshotLevel > 0.0);
|
|
}
|
|
}
|
|
if (charIsSticky) {
|
|
Stick(targetZed, hitLocation);
|
|
}
|
|
}
|
|
if (!class'NiceBulletAdapter'.static.ZedPenetration(
|
|
charDamage,
|
|
self,
|
|
targetZed,
|
|
(headshotLevel > 0.0),
|
|
(headshotLevel > charDamageType.default.prReqPrecise)
|
|
)
|
|
) {
|
|
charPenetrationCount += 1;
|
|
KillBullet();
|
|
}
|
|
}
|
|
|
|
function HandleScream(Vector disintegrationLocation, Vector entryDirection) {
|
|
if (!charIsDud) {
|
|
GenerateImpactEffects(disintegrationImpact, disintegrationLocation, entryDirection);
|
|
}
|
|
class'NiceBulletAdapter'.static.HandleScream(self, niceRI, disintegrationLocation, entryDirection);
|
|
}
|
|
|
|
function GenerateImpactEffects(
|
|
ImpactEffect effect,
|
|
Vector hitLocation,
|
|
Vector hitNormal,
|
|
optional bool bWallImpact,
|
|
optional bool bGenerateDecal
|
|
) {
|
|
local float actualCullDistance;
|
|
local float actualImpactShift;
|
|
local bool generatedEffect;
|
|
|
|
// No need to play visuals on a server, for a dead bullets or in case there's no local player at all
|
|
if (Level.NetMode == NM_DedicatedServer || bBulletDead || localPlayer == none) {
|
|
return;
|
|
}
|
|
if (!localPlayer.CanSpawnEffect(bGhost) && !effect.bImportanEffect) {
|
|
return;
|
|
}
|
|
// -- Classic effect
|
|
if (effect.bPlayROEffect && !bBulletDead) {
|
|
Spawn(class'ROBulletHitEffect',,, hitLocation, rotator(-hitNormal));
|
|
}
|
|
// -- Generate decal
|
|
if (bGenerateDecal && effect.decalClass != none) {
|
|
// Find appropriate cull distance for this decal
|
|
actualCullDistance = effect.decalClass.default.cullDistance;
|
|
// Double cull distance if local player is an instigator
|
|
if (instigator != none && localPlayer == instigator.Controller) {
|
|
actualCullDistance *= 2; // NICETODO: magic number
|
|
}
|
|
// Spawn decal
|
|
if (!localPlayer.BeyondViewDistance(hitLocation, actualCullDistance)) {
|
|
Spawn(effect.decalClass, self,, hitLocation, rotator(- hitNormal));
|
|
generatedEffect = true;
|
|
}
|
|
}
|
|
// -- Generate custom effect
|
|
if (effect.emitterClass != none && EffectIsRelevant(hitLocation, false)) {
|
|
if (bWallImpact) {
|
|
actualImpactShift = effect.emitterShiftWall;
|
|
} else {
|
|
actualImpactShift = effect.emitterShiftPawn;
|
|
}
|
|
Spawn(effect.emitterClass,,, hitLocation - movementDirection * actualImpactShift, rotator(movementDirection));
|
|
generatedEffect = true;
|
|
}
|
|
// -- Generate custom sound
|
|
if (effect.noise != none) {
|
|
class'NiceSoundCls'.default.effectSound = effect.noise;
|
|
class'NiceSoundCls'.default.effectVolume = effect.noiseVolume;
|
|
Spawn(class'NiceSoundCls',,, hitLocation);
|
|
generatedEffect = true;
|
|
}
|
|
if (generatedEffect) {
|
|
localPlayer.AddEffect();
|
|
}
|
|
}
|
|
|
|
function ShakeView(Vector hitLocation) {
|
|
local float distance, scale;
|
|
|
|
if (nicePlayer == none || shakeRadiusMult < 0.0) {
|
|
return;
|
|
}
|
|
distance = VSize(hitLocation - nicePlayer.ViewTarget.Location);
|
|
if (distance < charExplosionRadius * shakeRadiusMult) {
|
|
if (distance < charExplosionRadius) {
|
|
scale = 1.0;
|
|
} else {
|
|
scale = (charExplosionRadius * ShakeRadiusMult - distance) / (charExplosionRadius);
|
|
}
|
|
nicePlayer.ShakeView(
|
|
shakeRotMag * scale,
|
|
shakeRotRate,
|
|
shakeRotTime,
|
|
shakeOffsetMag * scale,
|
|
shakeOffsetRate,
|
|
shakeOffsetTime
|
|
);
|
|
}
|
|
}
|
|
|
|
function KillBullet() {
|
|
local int i;
|
|
|
|
if (bulletTrail != none) {
|
|
for (i = 0; i < bulletTrail.Emitters.Length; i ++) {
|
|
if (bulletTrail.Emitters[i] == none) {
|
|
continue;
|
|
}
|
|
bulletTrail.Emitters[i].ParticlesPerSecond = 0;
|
|
bulletTrail.Emitters[i].InitialParticlesPerSecond = 0;
|
|
bulletTrail.Emitters[i].RespawnDeadParticles = false;
|
|
}
|
|
bulletTrail.SetBase(none);
|
|
bulletTrail.AutoDestroy = true;
|
|
}
|
|
|
|
if (bulletXTrail != none) {
|
|
bulletXTrail.mRegen = false;
|
|
bulletXTrail.LifeSpan = LifeSpan;
|
|
}
|
|
bBulletDead = true;
|
|
bHidden = true;
|
|
SoundVolume = 0;
|
|
LifeSpan = FMin(LifeSpan, 0.1);
|
|
}
|
|
|
|
event Destroyed() {
|
|
KillBullet();
|
|
}
|
|
|
|
defaultproperties {
|
|
insideBouncesLeft=2
|
|
trajUpdFreq=0.100000
|
|
maxTraceCycles=128
|
|
bDisableComplexMovement=True
|
|
trailXClass=Class'KFMod.KFTracer'
|
|
regularImpact=(bPlayROEffect=True)
|
|
StaticMeshRef="kf_generic_sm.Shotgun_Pellet"
|
|
DrawType=DT_StaticMesh
|
|
bAcceptsProjectors=False
|
|
LifeSpan=15.000000
|
|
Texture=Texture'Engine.S_Camera'
|
|
bGameRelevant=True
|
|
bCanBeDamaged=True
|
|
SoundVolume=255
|
|
} |