//============================================================================== // NicePack / NiceFire //============================================================================== // New base class for fire modes, more fit for use with our 'bullet' system. // Functionality: // - Support for defining most information about bullet behaviour // - Support for burst fire // - Support for shooting different types of bullets at once // - Support for swappable shot types // - Support for increased damage when shooting in bursts // This class is setup to fit needs of as many weapons as possible with // minimal hassle, but some weapons might require additional work, most likely // including modification of 'AllowFire' and 'ConsumeAmmo' functions. // See classes of NicePack weapons and comment for 'IsMainFire' // for more details. //============================================================================== // Class hierarchy: Object > WeaponFire > InstantFire > KFFire > NiceFire //============================================================================== // 'Nice pack' source // Do whatever the fuck you want with it // Author: dkanus // E-mail: dkanus@gmail.com //============================================================================== class NiceFire extends KFFire dependson(NiceWeapon) abstract; //============================================================================== //============================================================================== // > Weapon fire parameters // All the parameters are contained in a structure that can be swapped // to alter the effects of the shooting via this mode. // These are supposed to be immutable;any skill that attempts to change them // should work with the copy of their value //============================================================================== // >> Weapon fire effects // Effects that weapon's context can have on bullets, // like bonus damage for firing continiously, // or ways bullets are fired, like bursting struct NWFWeaponType{ // How many bullets are fired at once? var int bulletsAmount; // How many ammo for one shot? var int ammoPerFire; // Can these bullets be fired with incomplete ammo? var bool bCanFireIncomplete; // Time windows between two shots var float fireRate; // -- These variables deal with increasing damage during continious damage // (this is used for ZCU's weapon mechanic) // Multiplier at which we increase our damage while firing continiously var float contFireBonusDamage; // Maximum of consequtive shots that can receive damage bonus var int maxBonusContLenght; // -- Are we firing in auto mode or are we bursting? var bool bAutoFire; // NICETODO: must update waitforrelease // This is auto-updated to be at least one after 'FillFireType' // to make semi-auto fire work as expected. var int maxBurstLength; }; //============================================================================== // >> Shooting rebound // Settings related to recoil, push-back from firing // or leaving ironsights on fire struct NWFRebound{ // Recoil rotation // NOTE: - in vanilla actual recoil was randomised between // provided angle and it's half; // - horizontal recoil still functions that way // - vertical recoil, however, is now a fixed value, // so setting it to the same value will result in higher actual // recoil (since randomization could only lower it) // - so if you want to preserve the same average recoil, then // set 'recoilVertical' to 75% of the value, // you'd use for vanilla var int recoilVertical, recoilHorizontal; // How much should firing move player around var Vector kickMomentum; // How much to scale up kick momentum in low grav so guys fly around var float lowGravKickMomentumScale; // Should we leave iron sight upon shooting? var bool bLeaveIronSights; }; //============================================================================== // >> Parameters of bullet spawning // Bullet parameters upon launch: offset, speed and spread struct NWFBulletMovement{ var class bulletClass; var Vector spawnOffset; var float speed; var float spread; // If set above zero, bullets will fall for that much time after hitting // a wall (similar to vanilla's behavior of blunt grenades and rockets) var float fallTime; }; //============================================================================== // >> Damage and other effects on zeds struct NWFBulletEffects{ // Damage that bullet should deal var int damage; // Momentum that should be transferred to zed var float momentum; // Damage type of this bullet var class shotDamageType; // Always deal original damage to zeds even after penetration var bool bCausePain; // Bullet should stick to zeds var bool bStickToZeds; // Bullet should stick to walls var bool bStickToWalls; // Should bullet be affected by scream? (being destroyed by default) var bool bAffectedByScream; // Should this projectile bounce off the walls? var bool bBounce; }; //============================================================================== // >> Explosion effects for this bullet struct NWFExplosionEffects{ // Damage type of the explosion var class damageType; // Maximum damage explosion can deal var int damage; // Maximum radius eplosion can affect var float radius; // Defines how fast damage falls off with distance // If we represent distance from explosion location // as number \alpha between 0 and 1: // ...1.0 is the location of explosion, // ...0.0 is any location on the border of explosion, // then \alpha^exponent would be the damage scale multiplier. // \alpha > 1 => damage quickly falls down when shifting from center // \alpha < 1 => more slowly var float exponent; // Momentum zeds would recieve from the explosion var float momentum; // Time from launch untill the fuse going off var float fuseTime; // Minimal distance at which we can detonate var float minArmDistance; // When should projectile explode? var bool bOnFuse; var bool bOnPawnHit; var bool bOnWallHit; }; //============================================================================== // >> Huge structure that combines everything that descibes our fire type struct NWFireType{ var string fireTypeName; var NWFRebound rebound; var NWFWeaponType weapon; var NWFBulletMovement movement; var NWFBulletEffects bullet; var NWFExplosionEffects explosion; }; var array fireTypes; var int curFireType; var NiceWeapon.FireAnimationSet fireAnims; //============================================================================== //============================================================================== // > State of this fire mode // These parameters are either supposed to change over the course of the game //============================================================================== // >> Contains variables describing most basic and low-level state variables struct NWFCBaseState{ // This fire mode is disabled and won't shoot var bool bDisabled; // If 'true' semi-auto fire will cause burst fire var bool bSemiIsBurst; // How much time should pass before player can fire again var float fireCooldown; // Set this flag to 'true' to disable recoil for the next shot; // it will be automatically reset to 'false' after that var bool bResetRecoil; // Did we shoot after latest fire button press? // Always 'false' when fire button is released var bool bShotOnClick; // Latest played fire animation and fire rate for which it is playing, // stored to correctly update it's speed; var NiceWeapon.FireAnim latestFireAnim; var float latestFireRate; // Amount of time yet to pass before we can replicate input flags again var float flagReplicationCooldown; // Instigator, controller and weapon used for firing var NiceWeapon sourceWeapon; var NiceHumanPawn instigator; var NicePlayerController instigatorCtrl; }; //============================================================================== // >> Describes current lock-on status struct NWFCLockonState{ var NiceMonster target; var float time; }; //============================================================================== // >> Describes status of bursting struct NWFCBurstState{ // Are we actively bursting right now? var bool bIsActive; // Shots in the burst we've made so far var int shotsMade; }; //============================================================================== // >> Describes status of continious fire struct NWFCAutoFireState{ var float accumulatedBonus; var int lenght; }; //============================================================================== // >> Contains all the information about context of this weapon fire struct NWCFireState{ var NWFCBaseState base; var NWFCLockonState lockon; var NWFCBurstState burst; var NWFCAutoFireState autoFire; }; var NWCFireState fireState; simulated function PostBeginPlay(){ local int i; super.PostBeginPlay(); FillFireType(); fireState.autoFire.accumulatedBonus = 1.0; // Burst fire is semi-auto fire, so it's length must be at least 1 for(i = 0;i < fireTypes.length;i ++) if(fireTypes[i].weapon.maxBurstLength < 1) fireTypes[i].weapon.maxBurstLength = 1; } simulated function FillFireType(){ } // Returns true if this fire mode is 'Main' // - Fire mode is considered 'main' if they use 'magAmmoRemaining' // and 'magAmmoRemainingClient' for keeping track of loaded ammo; // - the rest are non-'main' fire modes and they're assumed // to use 'secondaryCharge' instead. // // The default rules for deciding whether we're 'main' mode are: // 1. All primary fire modes are 'main'. // 2. All secondary fire modes without their own ammo are also 'main' // (since it's likely just another way to fire // the same ammo from the same mag) // 3. Everything else is non-'main' fire mode // // This distinction is introduced, // because almost all weapons can fit in this model: // 1. Simple rifles and shotguns require one 'main' fire mode and // 'magAmmoRemaining'/'magAmmoRemainingClient' variables for // storing currently loaded and ready for shooting ammo. // 2. Assault rifles (as well as nailgun oe hunting shotgun) can have two // fire modes, that still take ammo from the same source/magazine, // so both of their fire modes are 'main'. // 3. Something like M4M203 has two fifferent fire modes with two different // ammo pools, which can't share the same magazine; // for secondary, nade, fire 'secondaryCharge' is used to denote // whether or not nade is loaded. // 4. Medic guns fire darts as their secondary fire and also can't draw // from the same ammo pool, so they should use 'secondaryCharge'; // but standart rules define them as 'main', so they have to change // 'AllowFire' and 'ConsumeAmmo' functions. simulated function bool IsMainFire(){ return thisModeNum == 0 || !fireState.base.sourceWeapon.bHasSecondaryAmmo; } // All weapons should have 100% accuracy anyway simulated function AccuracyUpdate(float Velocity){} // Returns currently active fire type simulated function NWFireType GetFireType(){ if(curFireType < 0 || curFireType >= fireTypes.length) return fireTypes[0]; return fireTypes[curFireType]; } simulated function int GetBurstLength(){ return GetFireType().weapon.maxBurstLength; } simulated function StopBursting(){ fireState.burst.bIsActive = false; fireState.burst.shotsMade = 0; } // Tests whether fire button, corresponding to our fire mode is pressed atm. simulated function bool IsFireButtonPressed(){ local NicePlayerController nicePlayer; nicePlayer = fireState.base.instigatorCtrl; if(nicePlayer == none) return false; if(instigator.role == Role_AUTHORITY) return (thisModeNum == 0 && nicePlayer.bNiceFire == 1) || (thisModeNum == 1 && nicePlayer.bNiceAltFire == 1); else return (thisModeNum == 0 && nicePlayer.bFire == 1) || (thisModeNum == 1 && nicePlayer.bAltFire == 1); } simulated function TryReplicatingInputFlags(float delta){ local NicePlayerController nicePlayer; // If server already has current version of the flags - // there's nothing to replicate nicePlayer = fireState.base.instigatorCtrl; if(nicePlayer == none) return; if( nicePlayer.bFire == nicePlayer.bNiceFire && nicePlayer.bAltFire == nicePlayer.bNiceAltFire) return; // Otherwise - check cooldown and replicate if it's over fireState.base.flagReplicationCooldown -= delta / level.timeDilation; if(fireState.base.flagReplicationCooldown <= 0){ fireState.base.flagReplicationCooldown = 0.1; nicePlayer.bNiceFire = nicePlayer.bFire; nicePlayer.bNiceAltFire = nicePlayer.bAltFire; nicePlayer.ServerSetFireFlags(nicePlayer.bFire, nicePlayer.bAltFire); } } simulated function ModeTick(float delta){ local float headAimLevel; local NiceMonster currentTarget; if(instigator.role < Role_AUTHORITY) TryReplicatingInputFlags(delta); // Update instigator, controller and weapon, if necessary if(fireState.base.instigator == none) fireState.base.instigator = NiceHumanPawn(instigator); else if(fireState.base.instigatorCtrl == none) fireState.base.instigatorCtrl = NicePlayerController(instigator.controller); if(fireState.base.sourceWeapon == none) fireState.base.sourceWeapon = NiceWeapon(weapon); // Update lock-on if(instigator.role < Role_AUTHORITY){ headAimLevel = 0.0;//TraceZed(currentTarget); if(headAimLevel <= 0.0 || currentTarget == none){ fireState.lockon.time = 0.0; fireState.lockon.target = none; } else{ if(currentTarget == fireState.lockon.target) fireState.lockon.time += delta ; else fireState.lockon.time = 0.0; fireState.lockon.target = currentTarget; } } // Reset continious fire length here to make sure if(instigator.controller.bFire == 0 && instigator.controller.bAltFire == 0){ fireState.autoFire.lenght = 0; fireState.autoFire.accumulatedBonus = 1.0; } HandleFiring(delta); super.ModeTick(delta); } // NICETODO: rewrite StopFire function to force it to also stop burst fire simulated function HandleFiring(float delta){ // These flags represent 3 conditions that must be satisfied for // shooting attempt yo even be attempted: // 'bFirePressed': is fire button pressed? // 'bFireExpected': sometimes (ex. semiauto weapons) weapons shouldn't fire // even thout fire button is pressed // 'bCooldownPassed' has cooldown passed? local bool bFirePressed, bFireExpected, bCooldownPassed; // Did we fire the weapon? local bool shotFired; local float currentFireSpeed; // Check if we're pressing the fire button; // if not - reset flag that says we've shot during latest button press; // if we're bursting - emulate button press (but still reset a flag) bFirePressed = IsFireButtonPressed(); if(!bFirePressed) fireState.base.bShotOnClick = false; bFirePressed = bFirePressed || fireState.burst.bIsActive; // Firing is only expected if we're auto firing, bursting or // haven't yet shot during latest fire button press bFireExpected = GetFireType().weapon.bAutoFire; bFireExpected = bFireExpected || fireState.burst.bIsActive; bFireExpected = bFireExpected || !fireState.base.bShotOnClick; // Temporarily extend 'delta' time period according to fire speed; // we need to decrease it back after reducing cooldown to avoid any // multiplication stacking from recursion. currentFireSpeed = GetFireSpeed(); delta *= currentFireSpeed; bCooldownPassed = ReduceCooldown(delta); delta /= currentFireSpeed; // Fire if all the flags are set to 'true' if(bCooldownPassed && bFirePressed && bFireExpected) shotFired = NiceModeDoFire(); // If shot was actually fired - update fire state and cooldown; if(shotFired){ // Update appropriate state variable UpdateContiniousFire(bFirePressed); fireState.base.bShotOnClick = true; if(fireState.burst.bIsActive) fireState.burst.shotsMade ++; // New cooldown after shot DoFireCooldown(); // Try and shoot again, if there's any time left if(delta > 0.0) HandleFiring(delta); } // Otherwise - just update current animation else{ if(bCooldownPassed && fireState.base.latestFireAnim.bLoop) // Finish looped animation by playing appropriate fire end anim; // Waiting for cooldown may, in theory, lead to issues // with some weapons, but since looped fire animations are only // used for high rate of fire ones, it should be fine FinishFireLoopAnimation(); else UpdateFireAnimation(); } } // This function is called when next fire time needs to be updated simulated function DoFireCooldown(){ // - If we aren't bursting - simply set cooldown to 'fireRate' if(!fireState.burst.bIsActive){ fireState.base.fireCooldown = GetFireType().weapon.fireRate; return; } // - If we're bursting, we have two cases: // 1. It's not a final burst, so we cut burst time to fir all // shots into regular fire time if(fireState.burst.shotsMade < GetBurstLength()){ fireState.base.fireCooldown = GetFireType().weapon.fireRate / GetBurstLength(); return; } // 2. It was a final burst, so we set a slightly increased cooldown fireState.base.fireCooldown = GetFireType().weapon.fireRate * fireState.burst.shotsMade * 1.3; StopBursting(); } // Reduces cooldown by 'using up' passed time given by 'deltaPassed'. // 'deltaPassed' will be reduced on the amount required to reduce // cooldown as much as possible; // returns 'true' if cooldown was finished simulated function bool ReduceCooldown(out float deltaPassed){ // If there's not enough time - use up all the 'deltaPassed' if(fireState.base.fireCooldown > deltaPassed){ fireState.base.fireCooldown -= deltaPassed; deltaPassed = 0.0; return false; } // If 'deltaPassed' is more than enough time - use up only part of it deltaPassed -= fireState.base.fireCooldown; fireState.base.fireCooldown = 0.0; return true; } simulated function bool AllowFire(){ local float magAmmo; local bool bLacksCharge, bLacksAmmo; local KFPawn kfPwn; local NWFWeaponType weaponType; weaponType = GetFireType().weapon; kfPwn = KFPawn(instigator); if(fireState.base.sourceWeapon == none || kfPwn == none) return false; // Reject weapons that are swapping variants if(fireState.base.sourceWeapon.variantSwapState != SWAP_NONE) return false; // Reject bursting for way too long if(fireState.burst.bIsActive && fireState.burst.shotsMade >= GetBurstLength()) return false; // By default we assume that 'primary' fire modes care about chambered // bullets and all the rest - about charge // -- Reject shots hen there's no chambered round (for primary fire) if(IsMainFire() && fireState.base.sourceWeapon.bHasChargePhase && !fireState.base.sourceWeapon.bRoundInChamber) return false; // -- Reject shot when there's not enough charge bLacksCharge = fireState.base.sourceWeapon.secondaryCharge < weaponType.ammoPerFire; if(!IsMainFire() && bLacksCharge && !weaponType.bCanFireIncomplete) return false; // Check reloading if(fireState.base.sourceWeapon.bIsReloading) return false; // Check ammo in the mag magAmmo = fireState.base.sourceWeapon.GetMagazineAmmo(); // - Need to have at least some ammo if(magAmmo < 1) return false; // - If still not enough for 1 shot - we must be able to fire incomplete bLacksAmmo = magAmmo < weaponType.ammoPerFire; if(bLacksAmmo && !weaponType.bCanFireIncomplete) return false; // Check pawn actions if(kfPwn.SecondaryItem != none || kfPwn.bThrowingNade) return false; return super(WeaponFire).AllowFire(); } // This function will cause weapon to burst simulated function DoBurst(){ if(fireState.base.fireCooldown > 0) return; if(fireState.burst.bIsActive || fireState.base.bShotOnClick) return; if(GetFireType().weapon.maxBurstLength <= 0) return; fireState.burst.bIsActive = true; fireState.burst.shotsMade = 0; } simulated function UpdateContiniousFire(bool bPlayerWantsToFire){ // Player stopped shooting for a moment - reset bonus if(!bPlayerWantsToFire){ fireState.autoFire.lenght = 0; fireState.autoFire.accumulatedBonus = 1.0; return; } fireState.autoFire.lenght ++; if(fireState.autoFire.lenght > GetFireType().weapon.maxBonusContLenght) fireState.autoFire.accumulatedBonus = 1.0; else fireState.autoFire.accumulatedBonus *= GetFireType().weapon.contFireBonusDamage; } event ModeDoFire(){} simulated function bool NiceModeDoFire(){ local float recoilMult; local int ammoToFire; if(instigator == none) return false; if(fireState.base.bDisabled) return false; if(fireState.base.sourceWeapon == none) return false; if(!AllowFire()) return false; // How much ammo should we fire? ammoToFire = GetFireType().weapon.ammoPerFire; if(GetFireType().weapon.bCanFireIncomplete) ammoToFire = Min(ammoToFire, fireState.base.sourceWeapon.GetMagazineAmmo()); // Do shooting effects from standart classes, the only thing that // should be really different is replaced 'DoFireEffect' MDFEffects(ammoToFire); if(instigator.role == Role_AUTHORITY){ MDFEffectsServer(ammoToFire); ServerPlayFiring(); } else{ // Compute right recoil recoilMult = 1.0; if(fireState.burst.bIsActive) recoilMult = recoilMult / GetBurstLength(); MDFEffectsClient(ammoToFire, recoilMult); } return true; } // Fire effects that should affect both client and server: simulated function MDFEffects(int ammoToFire){ if(fireState.base.sourceWeapon == none) return; // Decrease player's speed while firing if(weapon.owner != none && weapon.owner.Physics != PHYS_Falling){ if(GetFireType().weapon.fireRate > 0.25){ weapon.owner.Velocity.x *= 0.1; weapon.owner.Velocity.y *= 0.1; } else{ weapon.owner.Velocity.x *= 0.5; weapon.owner.Velocity.y *= 0.5; } } // 3rd person effects weapon.IncrementFlashCount(thisModeNum); // Fire should cause some weapons to zoom out if( GetFireType().rebound.bLeaveIronSights || fireState.base.sourceWeapon.reloadType == RTYPE_AUTO) fireState.base.sourceWeapon.ZoomOut(false); // Interrupt firing when we need to take another weapon // NICETODO: can fuck usup in the future, if we were to redo firing innards if(instigator.pendingWeapon != weapon && instigator.pendingWeapon != none){ bIsFiring = false; weapon.PutDown(); } // Actually launches bullets DoNiceFireEffect(ammoToFire); } // Fire effects that should only affect server. simulated function MDFEffectsServer(int ammoToFire){ local Vector eyesPoint; // Alert zeds about shooting instigator.MakeNoise(1.0); // Shoot buttons on ScrN testing grounds eyesPoint = instigator.location + instigator.EyePosition(); DoTraceHack(eyesPoint, AdjustAim(eyesPoint, 0.0)); } // Fire effects that should only affect shooting client. simulated function MDFEffectsClient(int ammoToFire, float recoilMult){ // 'true' if ammo is infinite local bool bUberAmmo; local NicePlayerController nicePlayer; if(instigator == none) return; nicePlayer = NicePlayerController(instigator.controller); if(nicePlayer == none) return; // Reduce ammo bUberAmmo = nicePlayer.IsZedTimeActive() && class'NiceVeterancyTypes' .static.hasSkill( nicePlayer, class'NiceSkillSharpshooterZEDHundredGauntlets'); if(!bUberAmmo) ReduceAmmoClient(ammoToFire); // Fire effects InitEffects(); ShakeView(); NicePlayFiring(ammoToFire < GetFireType().weapon.ammoPerFire); FlashMuzzleFlash(); StartMuzzleSmoke(); if(bDoClientRagdollShotFX && Weapon.Level.NetMode == NM_Client) DoClientOnlyFireEffect(); // Recoil HandleRecoil(recoilMult); } simulated function ReduceAmmoClient(int ammoToFire){ if(IsMainFire()) ReduceMainAmmoClient(ammoToFire); else ReduceNonMainAmmoClient(ammoToFire); } simulated function ReduceMainAmmoClient(int ammoToFire){ local NiceWeapon usedWeapon; usedWeapon = fireState.base.sourceWeapon; if(usedWeapon == none) return; // Reduce ammo and reset round in chambered // Doesn't hurt us to reset it even if weapon doesn't use it usedWeapon.MagAmmoRemainingClient -= ammoToFire; if(usedWeapon.MagAmmoRemainingClient <= 0){ usedWeapon.MagAmmoRemainingClient = 0; usedWeapon.bRoundInChamber = false; } // After introduction of alternative ammo types - // we must also reduce it's counton the client usedWeapon.ConsumeNiceAmmo( usedWeapon.ammoState[0], usedWeapon.availableAmmoTypes, 0, ammoToFire); // Magazine weapons autoload their ammo from the magazine if(usedWeapon.bRoundInChamber && usedWeapon.reloadType == RTYPE_MAG){ usedWeapon.AmmoStackPush( usedWeapon.ammoState[0], usedWeapon.ammoState[0].currentAmmoType); } // Force server's magazine size usedWeapon.ServerReduceMag( usedWeapon.MagAmmoRemainingClient, level.TimeSeconds, thisModeNum); } simulated function ReduceNonMainAmmoClient(int ammoToFire){ local NiceWeapon usedWeapon; usedWeapon = fireState.base.sourceWeapon; if(usedWeapon == none) return; usedWeapon.secondaryCharge -= ammoToFire; // After introduction of alternative ammo types - // we must also reduce it's counton the client usedWeapon.ConsumeNiceAmmo( usedWeapon.ammoState[1], usedWeapon.availableAmmoTypesSecondary, 1, ammoToFire); // Reduce secondary ammo in case we were using it usedWeapon.ServerReduceMag( usedWeapon.magAmmoRemainingClient, level.TimeSeconds, thisModeNum); // Reduce secondary charge usedWeapon.ServerSetSndCharge(usedWeapon.secondaryCharge); } // Finds appropriate animation. // Since required animation can be different under various conditions: // - incomplete shot (DB shotgun) // - whether we're aiming or not // we must choose it based on these conditions. function NiceWeapon.FireAnim GetCorrectAnim(bool bIncomplete, bool bAimed){ if(bAimed){ if(bIncomplete && weapon.HasAnim(fireAnims.incompleteAimed.anim)) return fireAnims.incompleteAimed; else if(weapon.HasAnim(fireAnims.aimed.anim)) return fireAnims.aimed; } else if(bIncomplete && weapon.HasAnim(fireAnims.incomplete.anim)) return fireAnims.incomplete; return fireAnims.justFire; } function PlayFiringAnim(bool bIncomplete){ local float animRate; // Find appropriate animation and speed to play it at fireState.base.latestFireRate = GetFireSpeed(); fireState.base.latestFireAnim = GetCorrectAnim(bIncomplete, kfWeap.bAimingRifle); animRate = fireState.base.latestFireAnim.rate * fireState.base.latestFireRate; // Play it // 'LoopAnim' won't reset animation to initial position, // so no need to check if we're already playing the same animation if(fireState.base.latestFireAnim.bLoop) weapon.LoopAnim(fireState.base.latestFireAnim.anim, animRate); else weapon.PlayAnim(fireState.base.latestFireAnim.anim, animRate); } // NICETODO: don't update animations after certain point // Updates the speed of current fire animation according to fire rate function UpdateFireAnimation(){ local bool fireRateChanged; local name seqName; local float oFrame, oRate; local float animRate; weapon.GetAnimParams(0, seqName, oFrame, oRate); fireRateChanged = (fireState.base.latestFireRate != GetFireSpeed()); // Update animation only if it's speed changed // AND it's still the same animation, since otherwise we may interfer with // what we shouldn't if(fireRateChanged && fireState.base.latestFireAnim.anim == seqName){ fireState.base.latestFireRate = GetFireSpeed(); animRate = fireState.base.latestFireAnim.rate * fireState.base.latestFireRate; if(fireState.base.latestFireAnim.bLoop) weapon.LoopAnim(seqName, animRate); else{ weapon.PlayAnim(seqName, animRate); weapon.SetAnimFrame(oFrame); } } } // Starts end animation for currently running looped fire animation; // doesn't nothing if current animation isn't looped fire animation. function FinishFireLoopAnimation(){ local name seqName; local float oFrame, oRate; // Are we looping and does end animation even exist? if(!fireState.base.latestFireAnim.bLoop) return; if(fireState.base.latestFireAnim.animEnd == '') return; // Was this animation initiated by us or something else? // Bail if by something else. weapon.GetAnimParams(0, seqName, oFrame, oRate); if(seqName != fireState.base.latestFireAnim.anim) return; weapon.PlayAnim(fireState.base.latestFireAnim.animEnd, 1.0, 0.1); } function PlayFiringSound(){ local bool bDoStereoSound; local sound correctSound; local float correctVolume; local float randPitch; randPitch = FRand() * randomPitchAdjustAmt; if(FRand() < 0.5) randPitch *= -1.0; if(stereoFireSound != none && kfWeap.instigator.IsLocallyControlled()) bDoStereoSound = kfWeap.instigator.IsFirstPerson(); if(bDoStereoSound){ correctSound = stereoFireSound; correctVolume = transientSoundVolume * 0.85; } else{ correctSound = fireSound; correctVolume = transientSoundVolume; } weapon.PlayOwnedSound( correctSound, SLOT_Interact, correctVolume,, transientSoundRadius, 1.0 + randPitch, false); } function NicePlayFiring(bool bIncomplete){ if(kfWeap == none || weapon.mesh == none) return; if(kfWeap.instigator == none) return; PlayFiringAnim(bIncomplete); PlayFiringSound(); ClientPlayForceFeedback(fireForce); } // How reoil multiplier should be modified. // Added as a place for skills to take effect. simulated function float ModRecoilMultiplier(float recoilMult){ local int stationarySeconds; local bool bSkillRecoilReset; local NicePlayerController nicePlayer; local NiceHumanPawn nicePawn; if(instigator != none){ nicePlayer = NicePlayerController(instigator.controller); nicePawn = NiceHumanPawn(instigator); } if(nicePawn == none || nicePlayer == none || nicePlayer.bFreeCamera) return 0.0; bSkillRecoilReset = (nicePlayer.IsZedTimeActive() && class'NiceVeterancyTypes'.static. hasSkill(nicePlayer, class'NiceSkillEnforcerZEDBarrage')); if(bSkillRecoilReset) recoilMult = 0.0; if(nicePawn.stationaryTime > 0.0 && class'NiceVeterancyTypes'.static. hasSkill(nicePlayer, class'NiceSkillHeavyStablePosition')){ stationarySeconds = Ceil(2 * nicePawn.stationaryTime) - 1; recoilMult *= 1.0 - stationarySeconds * class'NiceSkillHeavyStablePosition'.default.recoilDampeningBonus; recoilMult = FMax(0.0, recoilMult); } return recoilMult; } // Mods recoil based on player's current movement. // Treats falling in low gravity differently from regular falling // to reduce recoil increase effects. simulated function Rotator AdjustRecoilForVelocity(Rotator recoilRotation){ local Vector adjustedVelocity; local float adjustedSpeed; local float recoilIncrement; local bool bLowGrav; if(weapon == none) return recoilRotation; if(recoilVelocityScale <= 0) return recoilRotation; if(weapon.owner != none && weapon.owner.physics == PHYS_Falling) bLowGrav = weapon.owner.PhysicsVolume.gravity.Z > class'PhysicsVolume'.default.gravity.Z; // Treat low gravity as a special case if(bLowGrav){ adjustedVelocity = weapon.owner.velocity; // Ignore Z velocity in low grav so we don't get massive recoil adjustedVelocity.Z = 0; adjustedSpeed = VSize(adjustedVelocity); // Reduce the falling recoil in low grav recoilIncrement = adjustedSpeed * recoilVelocityScale * 0.5; } else recoilIncrement = VSize(weapon.owner.velocity) * recoilVelocityScale; recoilRotation.pitch += recoilIncrement; recoilRotation.yaw += recoilIncrement; return recoilRotation; } // Rendomize recoil and apply skills, movement, health and fire rate // modifiers to it simulated function HandleRecoil(float recoilMult){ local float recoilAngle; local Rotator newRecoilRotation; local NicePlayerController nicePlayer; if(weapon == none) return; if(instigator != none) nicePlayer = NicePlayerController(instigator.controller); if(nicePlayer == none || nicePlayer.bFreeCamera) return; // Skills recoil mod recoilMult = ModRecoilMultiplier(recoilMult); // Apply flag reset if(fireState.base.bResetRecoil) recoilMult = 0.0; fireState.base.bResetRecoil = false; // Generate random values for a recoil // (we now randomize only horizontal recoil) recoilAngle = GetFireType().rebound.recoilVertical; newRecoilRotation.pitch = recoilAngle; recoilAngle = GetFireType().rebound.recoilHorizontal; newRecoilRotation.yaw = RandRange(recoilAngle * 0.5, recoilAngle); if(Rand(2) == 1) newRecoilRotation.yaw *= -1; // Further increase it due to movement and low health newRecoilRotation = AdjustRecoilForVelocity(newRecoilRotation); newRecoilRotation.pitch += (instigator.healthMax / instigator.health * 5); newRecoilRotation.yaw += (instigator.healthMax / instigator.health * 5); newRecoilRotation *= recoilMult; // Scale it to the fire rate; // calibrating recoil speed seems rather meaningless, // so just set it to constant nicePlayer.SetRecoil(newRecoilRotation, 0.1 * level.timeDilation); } // Finds appropriate point to spawn a bullet at, taking in the consideration // spawn offset setting and whether or not weapon is centered // (any weapon is centered when aiming down sights). // // There might be a problem if bullet spawn point is too far, since then enemy // can fit between us and spawn point, making it impossible to hit it; // in that case function attempts to find some other point between us and enemy function Vector FindBulletSpawnPoint(){ local Vector bulletSpawn; local Vector eyesPoint; local Vector X, Y, Z; local Vector hitLocation, normal, offset; local Actor other; if(instigator == none || weapon == none || kfWeap == none) return Vect(0,0,0); eyesPoint = instigator.location + instigator.EyePosition(); offset = GetFireType().movement.spawnOffset; // Spawn point candidate ('bulletSpawn') weapon.GetViewAxes(X, Y, Z); bulletSpawn = eyesPoint + X * offset.X; if(!kfWeap.bAimingRifle && !weapon.WeaponCentered()) bulletSpawn = bulletSpawn + weapon.hand * Y * offset.Y + Z * offset.Z; // Try tracing to see if there's something between our eyes ('eyePoint') // and our spawn point candidate ('bulletSpawn'); // if there is - spawn bullet at the first point // where we hit it ('hitLocation') other = weapon.Trace(hitLocation, normal, bulletSpawn, eyesPoint, false); if(other != none) return hitLocation; // Otherwise - return our previous candiate return bulletSpawn; } // Applies kickback as required by specified NWFireType. function DoKickback(NWFireType fireType){ local bool bLowGravity; local Vector kickback; if(instigator == none || instigator.role < Role_AUTHORITY) return; kickback = fireType.rebound.kickMomentum; bLowGravity = instigator.physicsVolume.gravity.Z > class'PhysicsVolume'.default.gravity.Z; if(instigator.physics == PHYS_Falling && bLowGravity) kickback *= fireType.rebound.lowGravKickMomentumScale; instigator.AddVelocity(kickback >> instigator.GetViewRotation()); } function DoNiceFireEffect(int ammoToFire){ local Vector bulletSpawnPoint; local Rotator aim; local NWFireType fireType; local int bulletsToSpawn; if(instigator == none) return; // Find proper projectile spawn point fireType = GetFireType(); bulletSpawnPoint = FindBulletSpawnPoint(); aim = AdjustAim(bulletSpawnPoint, aimError); bulletsToSpawn = ammoToFire * fireType.weapon.bulletsAmount; // Fire actual bullets class'NiceBulletSpawner'.static.FireBullets(bulletsToSpawn, bulletSpawnPoint, aim, fireType.movement.spread, fireType, fireState); DoKickback(fireType); } // NICETODO: redo 'TraceZed' and 'TraceWall' // Hack to trigger buttons on ScrN's testing grounds function DoTraceHack(Vector start, Rotator dir){ local Actor other; local array hitPoints; local Vector dirVector, end; local Vector hitLocation, hitNormal; dirVector = Vector(dir); end = start + traceRange * dirVector; other = Instigator.HitPointTrace( hitLocation, hitNormal, end, hitPoints, start,, 1); if(Trigger(other) != none) other.TakeDamage(35, instigator, hitLocation, dirVector, damageType); } defaultproperties { aimerror=0.0 /*ProjPerFire=1 ProjectileSpeed=1524.000000 MaxBurstLength=3 bulletClass=Class'NicePack.NiceBullet' contBonus=1.200000 maxBonusContLenght=1 AmmoPerFire=1 bRecoilRightOnly=false fireState.base.sourceWeapon=none*/ }