2427 lines
97 KiB
Ucode
2427 lines
97 KiB
Ucode
// Weapon class for NicePack that supports:
|
|
// 1. Client-side ammo management
|
|
// When using client-side hit detection we don't want to wait for server to send us an update on current ammunition amount.
|
|
// So we try and keep track of it on the client instead.
|
|
// This is especially important for high fire rate weapons.
|
|
// No protection against cheating implemented.
|
|
// 2. Client-side multistage reload
|
|
// Reloading process is now handled on the client and is tightly tied to the animation. Server only receives updates on ammo amount after reload.
|
|
// Reloading now also consists of several stages, which allows to update ammo amount at certain points of the animation and, coupled with reload cancellation, allows for partial completion of reload.
|
|
// Stages must be manually set for each weapon by specifying animation frame at which each stage should begin. Also a name of the bone, corresponding to the weapon's magazine must be provided.
|
|
// 1) Magazine weapons
|
|
// Magazine weapons have up to 4 reload stages:
|
|
// - First is a 'prestage', when magazine hasn't yet been removed,
|
|
// - Second is a main stage during which old magazine was removed, but new one wasn't yet inserted,
|
|
// - Third is weapon charging stage that happens after magazine insertion,
|
|
// - Fourth stage contains all the useless animation left-overs.
|
|
// 2) Shell-by-shell reload ('single') doesn't really has stages.
|
|
// The only "feature" for this type of reload is playing the end of the reload animation in case we're reloading from 0 ammo
|
|
// 3) Auto reload
|
|
// This describes the type of reload that is a part of a shooting animation for 1-shot weapons (and hunting shotgun).
|
|
// It consists of 3 stages:
|
|
// - First is unskippable part, usually the shot itself,
|
|
// - Second is actual reloading part,
|
|
// - Third is a trash part, that contains all the useless animation left-overs.
|
|
// Introduced functionality allows to force reload by skipping straight to stage 2.
|
|
// Unlike 2 previous reloads includes a stage that cannot (at least shouldn't) be interrupted by any means.
|
|
// 3. Reload cancellation
|
|
// Initially this functionality was just a copy-paste of a1eat0r's 'Reload options' mutator.
|
|
// Now functionality was altered to use introduced stages of reload process.
|
|
class NiceWeapon extends KFWeapon
|
|
dependson(NicePlainData)
|
|
abstract;
|
|
|
|
var float lastHeadshotTime;
|
|
var float ardourTimeout;
|
|
var int quickHeadshots;
|
|
|
|
var float holsteredCompletition;
|
|
|
|
var bool bLoadResourcesAsMaterial; // Force to load all graphic resources as just materials
|
|
var bool bUseFlashlightToToggle;
|
|
var int MagAmmoRemainingClient; // Tracks magazine size on client
|
|
var bool bRoundInChamber; // Indicates that bullet was loaded in a chamber
|
|
var float LastMagUpdateFromClient; // Time of the most recent magazine update, received from the client
|
|
var float ReloadDeadLine; // Time, after which server should speed-up reload
|
|
var float lastRecordedReloadRate;
|
|
var float recordedZoomTime;
|
|
|
|
// Weapon secondary charge counter
|
|
var bool bShowSecondaryCharge;
|
|
var int secondaryCharge;
|
|
|
|
// HUD icons changes
|
|
var bool bChangeClipIcon;
|
|
var bool bChangeBulletsIcon;
|
|
var bool bChangeSecondaryIcon;
|
|
var Texture hudClipTexture;
|
|
var Texture hudBulletsTexture;
|
|
var Texture hudSecondaryTexture;
|
|
|
|
// Laser-related variables
|
|
var bool bLaserActive; // The laser site is active
|
|
var bool bAllowFreeDot; // Allows dot from the laser to freely follow movements of the bone (the same way as it usually does during reload)
|
|
var() class<InventoryAttachment> LaserAttachmentClass; // First person laser attachment class
|
|
var Actor LaserAttachment; // First person laser attachment
|
|
var Actor altLaserAttachment; // Alternative laser attachment
|
|
var() byte LaserType; // current laser type
|
|
var Vector LaserAttachmentOffset; // relative offset from attachment bone
|
|
var Vector altLaserAttachmentOffset; // relative offset from alternative attachment bone
|
|
var Rotator LaserAttachmentRotation; // How should we rotate the bone, our laser is attached to?
|
|
var Rotator altLaserAttachmentRotation; // How should we rotate the bone, our alternative laser is attached to?
|
|
var const class<ScrnLocalLaserDot> LaserDotClass;
|
|
var ScrnLocalLaserDot LaserDot;
|
|
var ScrnLocalLaserDot altLaserDot;
|
|
var name LaserAttachmentBone;
|
|
var name altLaserAttachmentBone;
|
|
|
|
// Prossible reasons for reload cancel
|
|
enum ERelCancelCause{
|
|
CANCEL_FIRE,
|
|
CANCEL_ALTFIRE,
|
|
CANCEL_NADE,
|
|
CANCEL_COOKEDNADE,
|
|
CANCEL_SWITCH,
|
|
CANCEL_PASSIVESWITCH,
|
|
CANCEL_AIM
|
|
};
|
|
|
|
// Possible reload types for main reload (forced by 'ReloadMeNow()' function)
|
|
// Note that weapons with two fire-modes can have 'RTYPE_MAG' or 'RTYPE_SINGLE' for main reload and still use auto reload (e.g. for secondary fire of M4 203)
|
|
enum ERelType{
|
|
RTYPE_MAG, // Magazine-type reload (like for commando rifles or M14 EBR)
|
|
RTYPE_SINGLE, // Single-shell reload (like lar or shotgun)
|
|
RTYPE_AUTO // Means that auto reload is a main reload for this weapon (hunting shotgun and 1-shot weapons such as M79, M99, xbow, etc)
|
|
};
|
|
var ERelType reloadType;
|
|
|
|
// Possible stages of a magazine reload
|
|
enum ERelStage{
|
|
RSTAGE_NONE, // Weapon isn't being reloaded
|
|
RSTAGE_PREREL, // Magazine wasn't yet removed, reload can be safely interrupted
|
|
RSTAGE_MAINREL, // Magazine was already removed, reload cannot be interrupted without penalties
|
|
RSTAGE_POSTREL, // Magazine was replaced, reload can be safely interrupted, but post-reload stage (weapon charging) will have to be redone
|
|
RSTAGE_TRASH // Non-functioning frames of animation after inserting magazine and charging weapon
|
|
};
|
|
|
|
var bool bGiveObsessiveBonus;
|
|
// Magazine reload-related variables
|
|
var float reloadPreEndFrame; // Frame number, after which magazine is removed, so interrupting reload will result in zero magazine ammo
|
|
var float reloadEndFrame; // Frame number, after which new magazine is inserted, but gun wasn't yet charged, so if reload is interrupted after this stage, - charging would need to be redone later
|
|
var float reloadChargeEndFrame; // Frame number, after which weapon's reload can be interrupted without any penalties
|
|
var float reloadMagStartFrame; // Frame number, from which to start animation magazine insertion; don't confuse it with a point at which magazine removal starts in a full reload animation
|
|
var float reloadChargeStartFrame; // Frame number, from which to start animating weapon charging; don't confuse it with a point at which charging starts in a full reload animation
|
|
var bool bMagazineOut; // Indicates if magazine is currently removed from the weapon
|
|
var name magazineBone; // Bone that we need to hide when magazine is out
|
|
var bool bHasChargePhase; // Weapon needs to be charged at some point
|
|
var bool bNeedToCharge; // This flag marks the need to finish post-stage of reload (gun charging stage)
|
|
var ERelStage currentRelStage; // Current stage of the magazine reload
|
|
// Following variables are a result of poor initial design of this system that didn't account for the need to do reloads of dual weapons
|
|
// The whole thing will be rewritten from the ground up, but for now that's far from priority goal
|
|
// What I'm adding with these is an event system that would call a 'ReloadEvent' function when reload passes a certain point
|
|
// Only magazine reload supports these
|
|
struct EventRecord{
|
|
var string eventName;
|
|
var float eventFrame;
|
|
};
|
|
var array<EventRecord> relEvents;
|
|
var float lastEventCheckFrame;
|
|
|
|
// Single reload-related variables
|
|
var int subReloadStage; // Substage is part of animation between two consecutive shell loadings; they're numbered starting from zero
|
|
var bool alwaysPlayAnimEnd; // Should we always force playing end of the reload animation?
|
|
var array<float> reloadStages; // Array of frame numbers that indicate moments when ammo should be added
|
|
|
|
// Auto reload-related structure, enum and variables
|
|
// Auto reload activates by itself whenever appropriate animation starts to play; structure object below must be provided for each such animation
|
|
struct AutoReloadAnimDesc{
|
|
var name animName; // Name of the animation for which instance of this struct is prepared
|
|
var float canInterruptFrame; // Frame, starting from which reload can be interrupted
|
|
var float trashStartFrame; // Frame from which starts useless part of animation
|
|
var float resumeFrame; // Frame from which we must resume reload if it was previously interrupted
|
|
var float speedFrame; // Frame from which we must apply reload speed bonus (so that initial, shooting part remains unaffected)
|
|
};
|
|
// Array that contains information about all possible animations that can treated as auto-reload
|
|
var array<AutoReloadAnimDesc> autoReloadsDescriptions;
|
|
enum EAutoRelStage{
|
|
RAUTOSTAGE_NONE, // Auto reload is inactive
|
|
RAUTOSTAGE_UNINTERRUPTIBLE, // Auto reload is active and it's currently uninterruptible (usually that's the shooting part of animation)
|
|
RAUTOSTAGE_INTERRUPTIBLE // Auto reload is active and can be interrupted; ammo is reloaded at the end of this state (it will be skipped in case there's no more ammunition)
|
|
};
|
|
var bool bAllowAutoReloadSkip; // Indicates if switching to another weapon should allow player to skip interruptible part of auto reload completely
|
|
var int currentAutoReload; // Active auto reload always corresponds to a certain animation; this is index of it's corresponding data in 'autoReloadsDescriptions'
|
|
var EAutoRelStage currentAutoReloadStage;
|
|
var bool bAutoReload; // Indicates that current reload is the auto reload
|
|
var bool bAutoReloadInterrupted; // Indicates if auto reload was interrupted (and must be repeated as soon as possible)
|
|
var bool bAutoReloadPaused; // This is used to 'pause' auto reload in case we need to interrupt it for a moment (and then immediately continue from where we left), i.e. when throwing a grenade
|
|
var float autoReloadPauseFrame; // Frame at which current pause began
|
|
var bool bAutoReloadRateApplied; // Flag that remembers whether or not we've already applied reload speed up for current auto reload (to avoid constant animation's speed updates)
|
|
var float autoReloadSpeedModifier;
|
|
|
|
// Acrtive reload-related variables
|
|
// Active reload state
|
|
enum EActiveReloadState{
|
|
ACTR_NONE, // Activation wasn't yet attempted during current reload
|
|
ACTR_FAIL, // Activation failed
|
|
ACTR_SUCCESS // Activation succeeded
|
|
};
|
|
var bool bCanActiveReload; // Can we even use active reload with this weapon?
|
|
var float activeSlowdown; // How much should we slow down the speed of reload if player has failed?
|
|
var float activeSpeedup; // How much should we speedup active reload if player succeeded?
|
|
var EActiveReloadState activeReloadState; // Current state of active reload, only applicable during reload
|
|
var float activeWindow; // How long (0.0 is zero long, 1.0 is all animation) must be a time window during which you can activate active reload
|
|
|
|
enum EVariantSwapState{
|
|
SWAP_NONE,
|
|
SWAP_PUTDOWN,
|
|
SWAP_BRINGUP
|
|
};
|
|
var EVariantSwapState variantSwapState;
|
|
var float variantSwapCountdown;
|
|
var float variantSwapTime;
|
|
var string pendingVariant;
|
|
|
|
// We'll use this structure to handle different types of ammo for nice weapons,
|
|
// since I have no desire to mess with standart classes
|
|
// (but standart ammo system will be used for handling total ammo left)
|
|
//
|
|
// Describes general ammo type bought for all variants of the weapons;
|
|
// Which fire mode should be used is determined by a combination of
|
|
// 'NiceAmmoType' and 'WeaponVariant' via filling array of 'NiceAmmoEffects'
|
|
// structures in variant
|
|
struct NiceAmmoType{
|
|
// Name of this ammo type
|
|
var string ID;
|
|
// How much more 'ammo space' this ammo type take, compared to the default;
|
|
// If weapon can have 100 default ammo (mult. 1.0), then
|
|
// spaceMult=2.0 means you can have at most 50 of that ammo
|
|
// spaceMult=4.0 means you can have 25 of it.
|
|
// Using values blow 1.0 isn't recommended.
|
|
var float spaceMult;
|
|
// How much ammo of that typeplayer currently has.
|
|
var int amount;
|
|
};
|
|
|
|
// Each instance of this structure should belong to a variant, -
|
|
// then it links ammo type to appropriate fire type (both given by their index)
|
|
struct NiceAmmoEffects{
|
|
var string ammoID;
|
|
var string fireTypeID;
|
|
};
|
|
// Array of all available ammo type for this weapon;
|
|
// Those that player doesn't have are just set to zero.
|
|
var array<NiceAmmoType> availableAmmoTypes;
|
|
// Additional array for secondary ammo (if used in weapon)
|
|
var array<NiceAmmoType> availableAmmoTypesSecondary;
|
|
|
|
// Describes the kind of ammo that (should be) loaded into the gun;
|
|
// these variables are put into a separate structure for multiple fire modes &
|
|
// easier adaptation for dualies.
|
|
struct GunAmmoState{
|
|
// Reference to a fire object, corresponding to this state
|
|
var NiceFire linkedFire;
|
|
// Ammo type that would be loaded on the next reload
|
|
var string currentAmmoType;
|
|
// Stack of loaded ammo.
|
|
// - For magazine-based or one-shot weapons it'll contain 1 value when
|
|
// cocked/loaded and 0 otherwise;
|
|
// - For weapons that can be loaded shell-by-shell it contains values for every
|
|
// loaded bullet.
|
|
var array<string> loadedAmmoStack;
|
|
var int stackPointer;
|
|
// Duplicates current last element of the stack;
|
|
// Necessary to replicate information about current bullet to the server.
|
|
var string ammoStackLast;
|
|
};
|
|
// Ammo states for main and secondary fire
|
|
var GunAmmoState ammoState[2];
|
|
|
|
struct FireAnim{
|
|
var name anim;
|
|
var name animEnd;
|
|
var float rate;
|
|
var bool bLoop;
|
|
};
|
|
|
|
struct FireAnimationSet{
|
|
// Fire animations for different occasions
|
|
var FireAnim incomplete;
|
|
var FireAnim incompleteAimed;
|
|
var FireAnim aimed;
|
|
var FireAnim justFire;
|
|
};
|
|
|
|
// Sounds that weapon uses;
|
|
// separate structure is needed because wewant to set them both
|
|
// globally for a model and (optionally) on-per-skin basis
|
|
struct WeaponSoundSet{
|
|
// Selection sound
|
|
var string selectSoundRef;
|
|
var Sound selectSound;
|
|
// Fire sounds for both primary and alt fire
|
|
var string fireSoundRefs[2];
|
|
var Sound fireSounds[2];
|
|
// Stereo fire sounds for both primary (0) and alt fire (1)
|
|
var string stereoFireSoundRefs[2];
|
|
var Sound stereoFireSounds[2];
|
|
// No ammo sounds for both primary (0) and alt fire (1)
|
|
var string noAmmoSoundRefs[2];
|
|
var Sound noAmmoSounds[2];
|
|
// Weapon's ambient sound
|
|
var string ambientSoundRef;
|
|
var Sound ambientSound;
|
|
};
|
|
|
|
struct WeaponAnimationSet{
|
|
// 'Aimed' - animation is to be performed during aiming
|
|
// Idle animations
|
|
var name idleAnim;
|
|
var name idleAimedAnim;
|
|
// Weapon swap animations
|
|
var name selectAnim;
|
|
var name putDownAnim;
|
|
// Reload animation
|
|
var name reloadAnim;
|
|
// Reload animation for owner's pawn
|
|
var name reload3rdAnim;
|
|
// Fire animations for both our fire modes
|
|
var FireAnimationSet fireAnimSets[2];
|
|
};
|
|
|
|
// Describes texture and sound resources, related to a certain weapon's model
|
|
struct WeaponSkin{
|
|
// Skin name
|
|
var string ID;
|
|
// Force to load all textures as just materials
|
|
var bool bLoadResourcesAsMaterial;
|
|
// First-person skin textures
|
|
var array<string> paintRefs;
|
|
var array<Material> paints;
|
|
// Third-person attachment skin textures
|
|
var array<string> paint3rdRefs;
|
|
var array<Material> paints3rd;
|
|
// Sounds replacer
|
|
var bool bReplacesSounds;
|
|
};
|
|
|
|
// Describes how weapon should be displayed and animated
|
|
struct WeaponModel{
|
|
// Model name
|
|
var string ID;
|
|
// First person mesh
|
|
var string firstMeshRef;
|
|
var Mesh firstMesh;
|
|
// Attachment mesh
|
|
var string thirdMeshRef;
|
|
var Mesh thirdMesh;
|
|
// Image to be displayed on a HUD (unselected)
|
|
var string hudImageRef;
|
|
var Texture hudImage;
|
|
// Image to be displayed on a HUD (selected)
|
|
var string selectedHudImageRef;
|
|
var Texture selectedHudImage;
|
|
// Set of animations specific to this model
|
|
var WeaponAnimationSet animations;
|
|
// Skins available for this model
|
|
var array<WeaponSkin> skins;
|
|
// Appropriate sounds
|
|
var WeaponSoundSet soundSet;
|
|
// Variables affecting how weapon is displayed
|
|
// and where effects would be generated
|
|
var Vector playerViewOffset;
|
|
var Vector effectOffset;
|
|
};
|
|
|
|
// Describes how weapon should behaved and stores the list of
|
|
// the possible appearances
|
|
struct WeaponVariant{
|
|
var string ID;
|
|
var array<WeaponModel> models;
|
|
var array<NiceAmmoEffects> ammoEffects;
|
|
var array<NiceAmmoEffects> ammoEffectsSecondary;
|
|
};
|
|
var array<WeaponVariant> variants;
|
|
|
|
var int currentVariant;
|
|
var int currentModel, currentSkin;
|
|
// This default value increments each time model / skin changes;
|
|
// It being different from weapon's current value forces weapon to
|
|
// update it's visuals to current default values.
|
|
var int appearanceRev;
|
|
var bool bFilledAppearance;
|
|
|
|
replication{
|
|
reliable if(Role < ROLE_Authority)
|
|
ServerReduceMag, ServerSetMagSize, ServerSetSndCharge, ServerReload, ServerShiftReloadTime, ServerStopReload,
|
|
ServerSetCharging, ServerSetLaserType, ammoState;
|
|
reliable if(Role == ROLE_Authority)
|
|
ClientForceInterruptReload, ClientReloadMeNow, ClientSetMagSize, ClientSetSndCharge, ClientPutDown,
|
|
ClientThrowGrenade, ClientCookGrenade, ClientTryPendingWeapon, ClientUpdateWeaponMag, ClientSetLaserType,
|
|
ClientReloadAmmo;
|
|
reliable if(Role == ROLE_Authority)
|
|
holsteredCompletition, ardourTimeout;
|
|
}
|
|
|
|
// Tries to find 3 resource structures that fit given model and skin IDs,
|
|
// returns their indecies;
|
|
// If no models are defined - behaviour is undefined;
|
|
// If model is found, but has no defined skins - behaviour undefined;
|
|
// If model or skin with appropriate ID isn't found -
|
|
// returns zero (index of the first element);
|
|
// 'soundSetIndex' is chosen based on 'bReplacesSounds' in skin that
|
|
// this function has previously found.
|
|
static function FindResources( string modelID,
|
|
string skinID,
|
|
out int variantIndex,
|
|
out int modelIndex,
|
|
out int skinIndex){
|
|
local int i;
|
|
local WeaponVariant activeVariant;
|
|
local WeaponModel foundModel;
|
|
|
|
if(default.variants.length <= 0) return;
|
|
variantIndex = default.currentVariant;
|
|
activeVariant = default.variants[variantIndex];
|
|
if(activeVariant.models.length <= 0) return;
|
|
// Find model index , leave zero if found none
|
|
modelIndex = 0;
|
|
for(i = 0;i < activeVariant.models.length;i ++)
|
|
if(activeVariant.models[i].ID ~= modelID){
|
|
modelIndex = i;
|
|
break;
|
|
}
|
|
foundModel = activeVariant.models[modelIndex];
|
|
|
|
// Find skin index, leave zero if found none
|
|
if(foundModel.skins.length <= 0) return;
|
|
skinIndex = 0;
|
|
for(i = 0;i < foundModel.skins.length;i ++)
|
|
if(foundModel.skins[i].ID ~= skinID){
|
|
skinIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static function SetVariant(string newVariantID){
|
|
local int i;
|
|
local int newVariantIndex;
|
|
local string modelID;
|
|
local string skinID;
|
|
local WeaponVariant currentVariantStruct;
|
|
currentVariantStruct = default.variants[default.currentVariant];
|
|
if(currentVariantStruct.ID ~= newVariantID) return;
|
|
newVariantIndex = -1;
|
|
for(i = 0;i < default.variants.Length;i ++)
|
|
if(default.variants[i].ID ~= newVariantID){
|
|
newVariantIndex = i;
|
|
break;
|
|
}
|
|
if(newVariantIndex < 0) return;
|
|
modelID = currentVariantStruct.models[default.currentModel].ID;
|
|
skinID = currentVariantStruct.models[default.currentModel].
|
|
skins[default.currentSkin].ID;
|
|
default.currentVariant = newVariantIndex;
|
|
SetAppearance(modelID, skinID);
|
|
}
|
|
|
|
// Changes default variable values of weapon and fire classes
|
|
// for given model and skin IDs.
|
|
// Function also increments 'appearanceRev', causing weapon to update it's
|
|
// appearance to reflect our changes to default variables.
|
|
static function SetAppearance(string modelID, string skinID){
|
|
local int variantIndex;
|
|
local int modelIndex;
|
|
local int skinIndex;
|
|
local WeaponModel newModel;
|
|
// Find indeciece of appropriate model and skin
|
|
// (or default ones in case of error)
|
|
FindResources(modelID, skinID, variantIndex, modelIndex, skinIndex);
|
|
// Load model & it's skins
|
|
SetAppearanceModel(variantIndex, modelIndex);
|
|
SetAppearanceSkin(variantIndex, modelIndex, skinIndex);
|
|
newModel = default.variants[variantIndex].models[modelIndex];
|
|
// Load up it's sounds
|
|
SetAppearanceSoundSet(variantIndex, modelIndex);
|
|
SetAppearanceFireModeSoundSet(variantIndex, modelIndex, 0);
|
|
SetAppearanceFireModeSoundSet(variantIndex, modelIndex, 1);
|
|
// Update render parameters
|
|
default.playerViewOffset = newModel.playerViewOffset;
|
|
default.effectOffset = newModel.effectOffset;
|
|
// Do mark that we've just updated default values,
|
|
// - this will force weapon to update it's looks next time it can
|
|
default.appearanceRev ++;
|
|
}
|
|
|
|
// Helper subroutine for 'SetAppearance' that loads 1st-person resources
|
|
// general to provided 'WeaponModel'.
|
|
static function SetAppearanceModel(int variantIndex, int modelIndex){
|
|
local Object loadO;
|
|
// We'll work with it instead of something like 'models[modelIndex]'
|
|
// and paste result into 'models' array after we done.
|
|
local WeaponModel model;
|
|
model = default.variants[variantIndex].models[modelIndex];
|
|
|
|
// Load first person-related variables
|
|
// -- Mesh
|
|
if(model.firstMesh == none && model.firstMeshRef != ""){
|
|
loadO = DynamicLoadObject(model.firstMeshRef, class'SkeletalMesh');
|
|
model.firstMesh = SkeletalMesh(loadO);
|
|
}
|
|
if(default.mesh != model.firstMesh)
|
|
UpdateDefaultMesh(model.firstMesh);
|
|
// -- Hud image
|
|
if(model.hudImage == none && model.hudImageRef != ""){
|
|
loadO = DynamicLoadObject(model.hudImageRef, class'Texture');
|
|
model.hudImage = Texture(loadO);
|
|
}
|
|
default.hudImage = model.hudImage;
|
|
if(model.selectedHudImage == none && model.selectedHudImageRef != ""){
|
|
loadO = DynamicLoadObject(model.selectedHudImageRef, class'Texture');
|
|
model.selectedHudImage = Texture(loadO);
|
|
}
|
|
default.selectedHudImage = model.selectedHudImage;
|
|
// Remember loading these objects
|
|
default.variants[variantIndex].models[modelIndex] = model;
|
|
default.currentModel = modelIndex;
|
|
SetupDefaultAnimations(model.animations);
|
|
}
|
|
|
|
static function SetupDefaultAnimations(WeaponAnimationSet anims){
|
|
local int i;
|
|
local class<NiceFire> fireClass;
|
|
|
|
// Setup fire animations
|
|
for(i = 0;i <= 1;i ++){
|
|
fireClass = class<NiceFire>(default.fireModeClass[i]);
|
|
if(fireClass != none)
|
|
fireClass.default.fireAnims = anims.fireAnimSets[i];
|
|
}
|
|
|
|
// Setup the rest of weapon's animations
|
|
default.idleAnim = anims.idleAnim;
|
|
default.idleAimAnim = anims.idleAimedAnim;
|
|
default.selectAnim = anims.selectAnim;
|
|
default.putDownAnim = anims.putDownAnim;
|
|
default.reloadAnim = anims.reloadAnim;
|
|
default.weaponReloadAnim = anims.reload3rdAnim;
|
|
}
|
|
|
|
// Helper subroutine for 'SetAppearance' that loads 1st-person resources
|
|
// specific to provided 'WeaponSkin'.
|
|
static function SetAppearanceSkin( int variantIndex,
|
|
int modelIndex,
|
|
int skinIndex){
|
|
local int i;
|
|
local Object loadO;
|
|
// We'll work with it instead of something like
|
|
// 'default.models[modelIndex].skins[skinIndex]'
|
|
// and paste result into 'skins' array after we done.
|
|
local WeaponSkin skin;
|
|
skin = default.variants[variantIndex].models[modelIndex].skins[skinIndex];
|
|
|
|
// Load first person-related variables;
|
|
// for that go over each paint reference:
|
|
for(i = 0;i < skin.paintRefs.length;i ++){
|
|
// Skip null paint references
|
|
if(skin.paintRefs[i] == "") continue;
|
|
// Skip already loaded paints
|
|
if(i < skin.paints.length && skin.paints[i] != none) continue;
|
|
|
|
// Try to load current paint as various types of materials
|
|
skin.paints[i] = none;
|
|
if(!skin.bLoadResourcesAsMaterial){
|
|
loadO = DynamicLoadObject(skin.paintRefs[i], class'Combiner');
|
|
skin.paints[i] = Combiner(loadO);
|
|
}
|
|
if(skin.paints[i] == none && !skin.bLoadResourcesAsMaterial){
|
|
loadO = DynamicLoadObject(skin.paintRefs[i], class'FinalBlend');
|
|
skin.paints[i] = FinalBlend(loadO);
|
|
}
|
|
if(skin.paints[i] == none && !skin.bLoadResourcesAsMaterial){
|
|
loadO = DynamicLoadObject(skin.paintRefs[i], class'Shader');
|
|
skin.paints[i] = Shader(loadO);
|
|
}
|
|
if(skin.paints[i] == none && !skin.bLoadResourcesAsMaterial){
|
|
loadO = DynamicLoadObject(skin.paintRefs[i], class'Texture');
|
|
skin.paints[i] = Texture(loadO);
|
|
}
|
|
if(skin.paints[i] == none){
|
|
loadO = DynamicLoadObject(skin.paintRefs[i], class'Material');
|
|
skin.paints[i] = Material(loadO);
|
|
}
|
|
}
|
|
// Copy updated paints into default 'Skins' array
|
|
for(i = 0;i < skin.paintRefs.length;i ++)
|
|
if(skin.paints[i] != none || default.skins.length <= i)
|
|
default.skins[i] = skin.paints[i];
|
|
// Remember loading these objects
|
|
default.currentSkin = skinIndex;
|
|
default.variants[variantIndex].models[modelIndex].skins[skinIndex] = skin;
|
|
}
|
|
|
|
// Helper subroutine for 'SetAppearance' that loads sounds
|
|
// related to weapon itself.
|
|
static function SetAppearanceSoundSet(int variantIndex, int modelIndex){
|
|
local Object loadO;
|
|
// We'll work with it instead of something like 'soundSets[soundSetIndex]'
|
|
// and paste result into 'soundSets' array after we done.
|
|
local WeaponSoundSet soundSet;
|
|
soundSet = default.variants[variantIndex].models[modelIndex].soundSet;
|
|
if(soundSet.selectSound == none && soundSet.selectSoundRef != ""){
|
|
loadO = DynamicLoadObject(soundSet.selectSoundRef, class'Sound');
|
|
soundSet.selectSound = Sound(loadO);
|
|
}
|
|
default.selectSound = soundSet.selectSound;
|
|
// Remember loading these objects
|
|
default.variants[variantIndex].models[modelIndex].soundSet = soundSet;
|
|
}
|
|
|
|
// Helper subroutine for 'SetAppearance' that loads sounds
|
|
// specific to provided fire mode.
|
|
static function SetAppearanceFireModeSoundSet( int variantIndex,
|
|
int modelIndex,
|
|
int fireIndex){
|
|
local Object loadO;
|
|
local class<NiceFire> fireClass;
|
|
local string refToLoad;
|
|
// We'll work with it instead of something like 'soundSets[soundSetIndex]'
|
|
// and paste result into 'soundSets' array after we done.
|
|
local WeaponSoundSet soundSet;
|
|
soundSet = default.variants[variantIndex].models[modelIndex].soundSet;
|
|
|
|
// Attempt to find 'NiceFire'-type fire mode refered by 'fireIndex'
|
|
// and bail if we've failed.
|
|
if(fireIndex == 0 || fireIndex == 1)
|
|
fireClass = class<NiceFire>(default.fireModeClass[fireIndex]);
|
|
if(fireClass == none) return;
|
|
|
|
// Load sounds and update default values of 'fireClass';
|
|
// if possible (references are identical) -
|
|
// paste loaded sound into another fire mode.
|
|
//
|
|
// Above conditions conditions enforce that 'fireIndex' is equal to
|
|
// either '0' or '1', indicies of primary and alt fires in
|
|
// 'WeaponSoundSet' 's arrays respectively;
|
|
// this means we can use 'fireIndex' as index of these arrays that
|
|
// corresponds to sounds we must load
|
|
// and '1 - fireIndex' as index of other fire mode's sounds.
|
|
|
|
// -- Fire sound
|
|
refToLoad = soundSet.fireSoundRefs[fireIndex];
|
|
if( soundSet.fireSounds[fireIndex] == none && refToLoad != ""){
|
|
loadO = DynamicLoadObject(refToLoad, class'Sound'); // load up sound
|
|
soundSet.fireSounds[fireIndex] = Sound(loadO);
|
|
// fireSoundRefs[fireIndex] ~= fireSoundRefs[fireIndex],
|
|
// which means both fire modes share the same sound
|
|
if(refToLoad ~= soundSet.fireSoundRefs[1 - fireIndex]) // paste
|
|
soundSet.fireSounds[1 - fireIndex] = Sound(loadO);
|
|
}
|
|
fireClass.default.fireSound = soundSet.fireSounds[fireIndex];
|
|
|
|
// -- Stereo fire sound
|
|
refToLoad = soundSet.stereoFireSoundRefs[fireIndex];
|
|
if(soundSet.stereoFireSounds[fireIndex] == none && refToLoad != ""){
|
|
// load up sound
|
|
loadO = DynamicLoadObject(refToLoad, class'Sound');
|
|
soundSet.stereoFireSounds[fireIndex] = Sound(loadO);
|
|
// try pasting
|
|
if(refToLoad ~= soundSet.stereoFireSoundRefs[1 - fireIndex])
|
|
soundSet.stereoFireSounds[1 - fireIndex] = Sound(loadO);
|
|
}
|
|
fireClass.default.stereoFireSound = soundSet.stereoFireSounds[fireIndex];
|
|
|
|
// -- No ammo sound
|
|
refToLoad = soundSet.noAmmoSoundRefs[fireIndex];
|
|
if(soundSet.noAmmoSounds[fireIndex] == none && refToLoad != ""){
|
|
// load up sound
|
|
loadO = DynamicLoadObject(refToLoad, class'Sound');
|
|
soundSet.noAmmoSounds[fireIndex] = Sound(loadO);
|
|
// try pasting
|
|
if(refToLoad ~= soundSet.noAmmoSoundRefs[1 - fireIndex])
|
|
soundSet.noAmmoSounds[1 - fireIndex] = Sound(loadO);
|
|
}
|
|
fireClass.default.noAmmoSound = soundSet.noAmmoSounds[fireIndex];
|
|
// Remember loading these objects
|
|
default.variants[variantIndex].models[modelIndex].soundSet = soundSet;
|
|
}
|
|
|
|
// Run this function to update this weapon's resources to the current default
|
|
// values (those are supposed to be changed by 'SetAppearance' method).
|
|
// This function updates 'appearanceRev', preventing repeated updates.
|
|
simulated function UpdateAppearance(){
|
|
local int i;
|
|
if(appearanceRev >= default.appearanceRev) return;
|
|
|
|
// Update weapon resources
|
|
LinkMesh(default.mesh);
|
|
hudImage = default.hudImage;
|
|
selectedHudImage = default.selectedHudImage;
|
|
selectSound = default.selectSound;
|
|
for(i = 0;i < default.skins.length;i ++)
|
|
skins[i] = default.skins[i];
|
|
// Update weapon animations
|
|
idleAnim = default.idleAnim;
|
|
idleAimAnim = default.idleAimAnim;
|
|
selectAnim = default.selectAnim;
|
|
putDownAnim = default.putDownAnim;
|
|
reloadAnim = default.reloadAnim;
|
|
weaponReloadAnim = default.weaponReloadAnim;
|
|
// Update weapon render parameters
|
|
playerViewOffset = default.playerViewOffset;
|
|
effectOffset = default.effectOffset;
|
|
|
|
// Update fire mode's resources / animations
|
|
UpdateAppearanceFireMode(0);
|
|
UpdateAppearanceFireMode(1);
|
|
appearanceRev = default.appearanceRev;
|
|
}
|
|
|
|
// Helper function for 'UpdateAppearance'
|
|
// that updates given fire mode resources / animations.
|
|
// Doesn't update 'appearanceRev'.
|
|
simulated function UpdateAppearanceFireMode(int fireModeIndex){
|
|
local NiceFire fireModeInst;
|
|
if(appearanceRev >= default.appearanceRev) return;
|
|
fireModeInst = NiceFire(FireMode[fireModeIndex]);
|
|
if(fireModeInst == none) return;
|
|
|
|
fireModeInst.fireSound = fireModeInst.default.fireSound;
|
|
fireModeInst.stereoFireSound = fireModeInst.default.stereoFireSound;
|
|
fireModeInst.noAmmoSound = fireModeInst.default.noAmmoSound;
|
|
fireModeInst.fireAnims = fireModeInst.default.fireAnims;
|
|
}
|
|
|
|
static function FillInAppearance(){}
|
|
|
|
simulated function array<NiceAmmoType> GetAmmoTypesCopy(int fireM){
|
|
if(fireM == 0 || !bHasSecondaryAmmo)
|
|
return availableAmmoTypes;
|
|
else
|
|
return availableAmmoTypesSecondary;
|
|
}
|
|
|
|
simulated function string AmmoStackLookup(GunAmmoState ammoState){
|
|
if(instigator == none || ammoState.stackPointer < 0)
|
|
return ammoState.currentAmmoType;
|
|
if(instigator.role == ROLE_Authority)
|
|
return ammoState.ammoStackLast;
|
|
return ammoState.loadedAmmoStack[ammoState.stackPointer];
|
|
}
|
|
|
|
simulated function UpdateFireType(out GunAmmoState ammoState){
|
|
local int i;
|
|
local string currentAmmo;
|
|
local string appropFireType;
|
|
local array<NiceAmmoEffects> effects;
|
|
if(ammoState.linkedFire == none) return;
|
|
// Find current ammo
|
|
currentAmmo = AmmoStackLookup(ammoState);
|
|
// Find effects (name of fire type) corresponding
|
|
// to current ammo and variant
|
|
effects = variants[currentVariant].ammoEffects;
|
|
appropFireType = effects[0].fireTypeID;
|
|
for(i = 0;i < effects.length;i ++)
|
|
if(effects[i].ammoID ~= currentAmmo){
|
|
appropFireType = effects[i].fireTypeID;
|
|
break;
|
|
}
|
|
// Find index of found fire type and set it to be current fire type
|
|
for(i = 0;i < ammoState.linkedFire.fireTypes.length;i ++)
|
|
if(appropFireType ~= ammoState.linkedFire.fireTypes[i].fireTypeName){
|
|
ammoState.linkedFire.curFireType = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
simulated function AmmoStackPush(out GunAmmoState ammoState, string ammoType){
|
|
if(instigator == none || instigator.role == ROLE_Authority) return;
|
|
ammoState.stackPointer ++;
|
|
if(ammoState.stackPointer < 0)
|
|
ammoState.stackPointer = 0;
|
|
ammoState.loadedAmmoStack[ammoState.stackPointer] = ammoType;
|
|
ammoState.ammoStackLast = ammoType;
|
|
UpdateFireType(ammoState);
|
|
}
|
|
|
|
simulated function string AmmoStackPop(out GunAmmoState ammoState){
|
|
local string tipElement;
|
|
tipElement = AmmoStackLookup(ammoState);
|
|
ammoState.stackPointer --;
|
|
if(ammoState.stackPointer >= 0)
|
|
ammoState.ammoStackLast =
|
|
ammoState.loadedAmmoStack[ammoState.stackPointer];
|
|
UpdateFireType(ammoState);
|
|
return tipElement;
|
|
}
|
|
|
|
simulated function bool IsAmmoStackEmpty(GunAmmoState ammoState){
|
|
return (ammoState.stackPointer < 0);
|
|
}
|
|
|
|
// How much space takes up ammo we currently have.
|
|
// That's not a total ammo count, but the 'weight' of all ammo.
|
|
// If everything works correctly - it should be between 0 and 'MaxAmmo',
|
|
// but function doesn't try to fix situation if we've got too much ammo.
|
|
// Return value is guaranteed to be >= 0.
|
|
simulated function float GetCurrentAmmoSpace(int fireM){
|
|
local int i;
|
|
local float ammoSpace;
|
|
local array<NiceAmmoType> ammoTypes;
|
|
ammoTypes = GetAmmoTypesCopy(fireM);
|
|
for(i = 0;i < ammoTypes.length;i ++)
|
|
ammoSpace += ammoTypes[i].amount * ammoTypes[i].spaceMult;
|
|
return FMax(0.0, ammoSpace);
|
|
}
|
|
|
|
// Returns total ammo count for given fire mode.
|
|
simulated function int GetTotalAmmo(int fireM){
|
|
local int i;
|
|
local int totalAmmo;
|
|
local array<NiceAmmoType> ammoTypes;
|
|
ammoTypes = GetAmmoTypesCopy(fireM);
|
|
for(i = 0;i < ammoTypes.length;i ++)
|
|
totalAmmo += ammoTypes[i].amount;
|
|
return Max(0, totalAmmo);
|
|
}
|
|
|
|
// 'Fixes' all the nice ammo.
|
|
// This means we alter it's ammo so that it fits under 'MaxAmmo' restriction
|
|
// and syncs it with 'ammoAmount' values from vanilla ammo classes.
|
|
// The sync part is added for compatibility with mods that alter ammo counts;
|
|
// it's accomplished by making upthe difference via adding/removing
|
|
// default ammo.
|
|
simulated function FixNiceAmmo(){
|
|
// Fix ammo for primary mode
|
|
FixNiceAmmoTypes(availableAmmoTypes, 0);
|
|
// Fix ammo for secondary mode
|
|
if(!bHasSecondaryAmmo) return;
|
|
FixNiceAmmoTypes(availableAmmoTypesSecondary, 1);
|
|
}
|
|
|
|
// 'Fix' given nice ammo types for given fire mode.
|
|
// See 'FixNiceAmmo' for more details.
|
|
simulated function FixNiceAmmoTypes(out array<NiceAmmoType> types,
|
|
int fireM){
|
|
local int i;
|
|
local float difference;
|
|
local NiceAmmo allAmmo;
|
|
// Is there and vanilla-ammo for given fire mode? Bail if not.
|
|
allAmmo = NiceAmmo(ammo[fireM]);
|
|
if(allAmmo == none || types.length <= 0) return;
|
|
// If vanilla ammo count and total ammo are desynced -
|
|
// fix it by changing amount of the default ammo.
|
|
types[0].amount += allAmmo.ammoAmount - GetTotalAmmo(fireM);
|
|
// Throw away ammo until it fits in under 'MaxAmmo'
|
|
for(i = 0;i < types.length;i ++){
|
|
// Did we reach our objective?
|
|
// Bail if yes...
|
|
difference = allAmmo.MaxAmmo - GetCurrentAmmoSpace(0);
|
|
if(difference <= 0) break;
|
|
// ...otherwise throw away another ammo type
|
|
types[i].amount -= Ceil(difference / types[i].spaceMult);
|
|
if(types[i].amount < 0)
|
|
types[i].amount = 0.0;
|
|
}
|
|
}
|
|
|
|
// Finds index of ammo type from it's ID.
|
|
// Returns -1 if didn't find required ammo.
|
|
simulated function int FindNiceAmmoByID(string ID, int fireM){
|
|
local int i;
|
|
local array<NiceAmmoType> types;
|
|
types = GetAmmoTypesCopy(fireM);
|
|
for(i = 0;i < types.length;i ++)
|
|
if(ID ~= types[i].ID)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
simulated function ChangeAmmoType(string newAmmoType){
|
|
local int i;
|
|
local int ammoID;
|
|
for(i = 0;i < 1;i ++){
|
|
if(ammoState[i].linkedFire == none) continue;
|
|
ammoID = FindNiceAmmoByID(newAmmoType, i);
|
|
if(ammoID >= 0 && ammoState[i].linkedFire.AllowFire()){
|
|
ammoState[i].currentAmmoType = newAmmoType;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adds given amount to given ammo type.
|
|
simulated function AddNiceAmmo(string ammoType, int amount){
|
|
local int fireM;
|
|
local int ammoTypeIndex;
|
|
local float freeSpace;
|
|
ammoTypeIndex = FindNiceAmmoByID(ammoType, 0);
|
|
if(ammoTypeIndex < 0){
|
|
ammoTypeIndex = FindNiceAmmoByID(ammoType, 1);
|
|
if(ammoTypeIndex < 0) return;
|
|
fireM = 1;
|
|
}
|
|
if(NiceAmmo(Ammo[fireM]) == none) return;
|
|
freeSpace = NiceAmmo(Ammo[fireM]).MaxAmmo - GetCurrentAmmoSpace(fireM);
|
|
if(fireM == 0){
|
|
amount = FMin(amount,
|
|
freeSpace / availableAmmoTypes[ammoTypeIndex].spaceMult);
|
|
availableAmmoTypes[ammoTypeIndex].amount += amount;
|
|
}
|
|
else{
|
|
amount = FMin(amount,
|
|
freeSpace / availableAmmoTypesSecondary[ammoTypeIndex].spaceMult);
|
|
availableAmmoTypesSecondary[ammoTypeIndex].amount += amount;
|
|
}
|
|
FixNiceAmmo();
|
|
}
|
|
|
|
static function PreloadAssets(Inventory inv, optional bool bSkipRefCount){}
|
|
static function bool UnloadAssets(){return false;}
|
|
|
|
simulated function PostBeginPlay(){
|
|
if(Role < ROLE_Authority){
|
|
if(!default.bFilledAppearance){
|
|
FillInAppearance();
|
|
default.bFilledAppearance = true;
|
|
availableAmmoTypes = default.availableAmmoTypes;
|
|
variants = default.variants;
|
|
}
|
|
SetAppearance("", "");
|
|
}
|
|
if(default.recordedZoomTime < 0)
|
|
default.recordedZoomTime = ZoomTime;
|
|
recordedZoomTime = default.recordedZoomTime;
|
|
// Default variables
|
|
LastMagUpdateFromClient = 0.0;
|
|
bNeedToCharge = false;
|
|
bMagazineOut = false;
|
|
currentRelStage = RSTAGE_NONE;
|
|
lastEventCheckFrame = -1.0;
|
|
bRoundInChamber = false;
|
|
// Fill sub reload stages
|
|
fillSubReloadStages();
|
|
// Auto fill reload stages for 'RTYPE_SINGLE' reload
|
|
if(reloadType == RTYPE_SINGLE)
|
|
UpdateSingleReloadVars();
|
|
if(reloadType == RTYPE_AUTO)
|
|
bHasChargePhase = false;
|
|
if(reloadChargeStartFrame < 0.0 || reloadChargeEndFrame < 0.0)
|
|
bHasChargePhase = false;
|
|
super.PostBeginPlay();
|
|
ammoState[0].stackPointer = -1;
|
|
ammoState[1].stackPointer = -1;
|
|
ammoState[0].linkedFire = NiceFire(FireMode[0]);
|
|
ammoState[0].currentAmmoType = availableAmmoTypes[0].ID;
|
|
if(bHasSecondaryAmmo){
|
|
ammoState[1].linkedFire = NiceFire(FireMode[1]);
|
|
ammoState[1].currentAmmoType = availableAmmoTypesSecondary[0].ID;
|
|
}
|
|
else{
|
|
ammoState[1].linkedFire = NiceFire(FireMode[0]);
|
|
ammoState[1].currentAmmoType = availableAmmoTypes[0].ID;
|
|
}
|
|
}
|
|
|
|
|
|
// Allows to prevent leaving iron sights unwillingly
|
|
function bool ShouldLeaveIronsight(){
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
niceVet = class'NiceVeterancyTypes'.static.GetVeterancy(Instigator.PlayerReplicationInfo);
|
|
if(niceVet != none && niceVet.static.hasSkill(NicePlayerController(Instigator.Controller), class'NiceSkillEnforcerUnshakable'))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Updates max value for this weapon's ammunition
|
|
simulated function UpdateWeaponAmmunition(){
|
|
local int i;
|
|
local NiceAmmo ammoInstance;
|
|
for(i = 0;i < NUM_FIRE_MODES;i ++){
|
|
ammoInstance = NiceAmmo(Ammo[i]);
|
|
if(ammoInstance == none) continue;
|
|
ammoInstance.UpdateAmmoAmount();
|
|
}
|
|
FixNiceAmmo();
|
|
}
|
|
|
|
simulated function ClientUpdateWeaponMag(){
|
|
local int actualMag;
|
|
UpdateMagCapacity(Instigator.PlayerReplicationInfo);
|
|
actualMag = MagAmmoRemainingClient;
|
|
if(bHasChargePhase && bRoundInChamber && actualMag > 0)
|
|
actualMag --;
|
|
MagAmmoRemainingClient = Min(MagAmmoRemainingClient, MagCapacity);
|
|
if(bHasChargePhase && bRoundInChamber)
|
|
actualMag ++;
|
|
ServerSetMagSize(MagAmmoRemainingClient, bRoundInChamber, Level.TimeSeconds);
|
|
}
|
|
|
|
simulated function ConsumeNiceAmmo( out GunAmmoState ammoState,
|
|
out array<NiceAmmoType> ammoTypes,
|
|
int mode,
|
|
float load){
|
|
local int i;
|
|
local int ammoTypeIndex;
|
|
for(i = 0;i < load;i ++){
|
|
ammoTypeIndex = FindNiceAmmoByID(AmmoStackPop(ammoState), mode);
|
|
if(ammoTypeIndex < 0) continue;
|
|
ammoTypes[ammoTypeIndex].amount --;
|
|
if(ammoTypes[ammoTypeIndex].amount < 0)
|
|
ammoTypes[ammoTypeIndex].amount = 0;
|
|
}
|
|
}
|
|
|
|
// Overloaded to properly reduce magazine's size
|
|
simulated function bool ConsumeAmmo(int Mode, float Load, optional bool bAmountNeededIsMax){
|
|
local Inventory Inv;
|
|
local bool bOutOfAmmo;
|
|
local KFWeapon KFWeap;
|
|
if(super(Weapon).ConsumeAmmo(Mode, Load, bAmountNeededIsMax)){
|
|
if(Load > 0 && (Mode == 0 || !bHasSecondaryAmmo)){
|
|
MagAmmoRemaining -= Load;
|
|
if(MagAmmoRemaining < 0)
|
|
MagAmmoRemaining = 0;
|
|
if(MagAmmoRemaining <= 0)
|
|
bRoundInChamber = false;
|
|
}
|
|
|
|
NetUpdateTime = Level.TimeSeconds - 1;
|
|
|
|
if(FireMode[Mode].AmmoPerFire > 0 && InventoryGroup > 0 && !bMeleeWeapon && bConsumesPhysicalAmmo &&
|
|
(Ammo[0] == none || FireMode[0] == none || FireMode[0].AmmoPerFire <= 0 || Ammo[0].AmmoAmount < FireMode[0].AmmoPerFire) &&
|
|
(Ammo[1] == none || FireMode[1] == none || FireMode[1].AmmoPerFire <= 0 || Ammo[1].AmmoAmount < FireMode[1].AmmoPerFire)){
|
|
bOutOfAmmo = true;
|
|
|
|
for(Inv = Instigator.Inventory;Inv != none; Inv = Inv.Inventory){
|
|
KFWeap = KFWeapon(Inv);
|
|
|
|
if(Inv.InventoryGroup > 0 && KFWeap != none && !KFWeap.bMeleeWeapon && KFWeap.bConsumesPhysicalAmmo &&
|
|
((KFWeap.Ammo[0] != none && KFWeap.FireMode[0] != none && KFWeap.FireMode[0].AmmoPerFire > 0 &&KFWeap.Ammo[0].AmmoAmount >= KFWeap.FireMode[0].AmmoPerFire) ||
|
|
(KFWeap.Ammo[1] != none && KFWeap.FireMode[1] != none && KFWeap.FireMode[1].AmmoPerFire > 0 && KFWeap.Ammo[1].AmmoAmount >= KFWeap.FireMode[1].AmmoPerFire))){
|
|
bOutOfAmmo = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(bOutOfAmmo)
|
|
PlayerController(Instigator.Controller).Speech('AUTO', 3, "");
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Forces update for client's magazine ammo counter
|
|
// In case we are using client-side hit-detection, client itself manages remaining ammunition in magazine, but in some cases we want server to dictate current magazine amount
|
|
// This function sets client's mag size to a given value
|
|
simulated function ClientSetMagSize(int newMag, bool bChambered){
|
|
MagAmmoRemainingClient = newMag;
|
|
bRoundInChamber = bChambered;
|
|
if(MagAmmoRemainingClient > 0 && bHasChargePhase){
|
|
if(bRoundInChamber){
|
|
ammoState[0].stackPointer = -1;
|
|
AmmoStackPush(ammoState[0], ammoState[0].currentAmmoType);
|
|
}
|
|
else
|
|
bNeedToCharge = true;
|
|
}
|
|
}
|
|
|
|
// This function allows clients to change magazine size without altering total ammo amount
|
|
// It allows clients to provide time-stamps, so that older change won't override a newer one
|
|
function ServerSetMagSize(int newMag, bool bChambered, float updateTime){
|
|
magAmmoRemaining = newMag;
|
|
bRoundInChamber = bChambered;
|
|
if(LastMagUpdateFromClient <= updateTime)
|
|
LastMagUpdateFromClient = updateTime;
|
|
}
|
|
|
|
// This function allows clients to change magazine size along with total ammo amount on the server (to update ammo counter in client-side mode)
|
|
// It allows clients to provide time-stamps, so that older change won't override a newer one
|
|
// Intended to be used for decreasing ammo count from shooting and cannot increase magazine size
|
|
simulated function ServerReduceMag(int newMag, float updateTime, int Mode){
|
|
local int delta;
|
|
if(Mode == 0 || !bHasSecondaryAmmo){
|
|
delta = magAmmoRemaining - newMag;
|
|
// Only update later changes that actually decrease magazine
|
|
if(LastMagUpdateFromClient <= updateTime && delta > 0){
|
|
LastMagUpdateFromClient = updateTime;
|
|
ConsumeAmmo(Mode, delta);
|
|
}
|
|
}
|
|
else
|
|
ConsumeAmmo(Mode, 1);
|
|
}
|
|
|
|
// Forces either 'AddReloadedAmmo' or 'AddAutoReloadedAmmo' (depending on which one is appropriate) function on client
|
|
// Somewhat of a hack to allow server force-add ammo to the weapon
|
|
simulated function ClientReloadAmmo(){
|
|
if(reloadType == RTYPE_AUTO)
|
|
AddAutoReloadedAmmo();
|
|
else
|
|
AddReloadedAmmo();
|
|
bNeedToCharge = false;
|
|
bMagazineOut = false;
|
|
if(bHasChargePhase){
|
|
if(IsAmmoStackEmpty(ammoState[0])){
|
|
AmmoStackPush(ammoState[0], ammoState[0].currentAmmoType);
|
|
}
|
|
bRoundInChamber = true;
|
|
}
|
|
ResetReloadVars();
|
|
}
|
|
|
|
// Adds appropriate amount of ammo during reload.
|
|
// Up to full mag for magazine reload, 1 ammo per call for single reload.
|
|
// Isn't called in case of auto reload, use 'AddAutoReloadedAmmo' for that.
|
|
simulated function AddReloadedAmmo(){
|
|
UpdateMagCapacity(Instigator.PlayerReplicationInfo);
|
|
if(reloadType == RTYPE_MAG){
|
|
if(AmmoAmount(0) >= MagCapacity){
|
|
MagAmmoRemainingClient = MagCapacity;
|
|
if(bRoundInChamber)
|
|
MagAmmoRemainingClient ++;
|
|
}
|
|
else
|
|
MagAmmoRemainingClient = AmmoAmount(0);
|
|
}
|
|
else if(reloadType == RTYPE_SINGLE){
|
|
if(AmmoAmount(0) - MagAmmoRemainingClient > 0 && MagAmmoRemainingClient < MagCapacity){
|
|
MagAmmoRemainingClient ++;
|
|
AmmoStackPush(ammoState[0], ammoState[0].currentAmmoType);
|
|
}
|
|
}
|
|
ServerSetMagSize(MagAmmoRemainingClient, bRoundInChamber, Level.TimeSeconds);
|
|
}
|
|
|
|
// Function that manages ammo replenishing for auto reload
|
|
simulated function AddAutoReloadedAmmo(){
|
|
if(reloadType != RTYPE_AUTO){
|
|
secondaryCharge = 1;
|
|
AmmoStackPush(ammoState[1], ammoState[1].currentAmmoType);
|
|
ServerSetSndCharge(secondaryCharge);
|
|
}
|
|
else{
|
|
if(AmmoAmount(0) > 0){
|
|
MagAmmoRemainingClient = 1;
|
|
AmmoStackPush(ammoState[0], ammoState[0].currentAmmoType);
|
|
}
|
|
else
|
|
MagAmmoRemainingClient = 0;
|
|
ServerSetMagSize(MagAmmoRemainingClient, bRoundInChamber, Level.TimeSeconds);
|
|
}
|
|
}
|
|
|
|
simulated function float TimeUntillReload(){
|
|
if(reloadType == RTYPE_MAG)
|
|
return FMax(reloadChargeEndFrame, reloadEndFrame) * GetAnimDuration(ReloadAnim);
|
|
else if(reloadType == RTYPE_SINGLE && reloadStages.Length > 0)
|
|
return reloadStages[0] * GetAnimDuration(ReloadAnim);
|
|
else if(reloadType == RTYPE_AUTO && autoReloadsDescriptions.Length > 0)
|
|
return autoReloadsDescriptions[0].trashStartFrame * GetAnimDuration(autoReloadsDescriptions[0].animName);
|
|
return 60.0;
|
|
}
|
|
|
|
simulated function SwapVariant(string newVariant){
|
|
local float putDownAnimDuration;
|
|
if(variantSwapState != SWAP_NONE) return;
|
|
ClientForceInterruptReload(CANCEL_SWITCH);
|
|
if(bIsReloading) return;
|
|
putDownAnimDuration = GetAnimDuration(PutDownAnim);
|
|
PlayAnim(PutDownAnim, putDownAnimDuration / variantSwapTime);
|
|
variantSwapCountdown = variantSwapTime;
|
|
variantSwapState = SWAP_PUTDOWN;
|
|
pendingVariant = newVariant;
|
|
}
|
|
|
|
// Interrupt the reloading animation, which may reset the weapon's magAmmoRemaining;
|
|
// Don't allow Fire() and AltFire() to interrupt by shooting with empty magazine.
|
|
simulated function ClientForceInterruptReload(ERelCancelCause cause){
|
|
local bool bActualAllowAutoReloadSkip;
|
|
local bool bCauseFire, bCauseAltFire, bCauseNade, bCauseAim, bCauseSwitch;
|
|
local HUDKillingFloor HUD;
|
|
local NiceHumanPawn nicePawn;
|
|
local NicePlayerController nicePlayer;
|
|
local bool bDisplayInventory, bWeReallyShould;
|
|
// Determine causes flags
|
|
bCauseFire = cause == CANCEL_FIRE;
|
|
bCauseAltFire = cause == CANCEL_ALTFIRE;
|
|
bCauseNade = (cause == CANCEL_NADE) || (cause == CANCEL_COOKEDNADE);
|
|
bCauseAim = cause == CANCEL_AIM;
|
|
bCauseSwitch = cause == CANCEL_SWITCH || cause == CANCEL_PASSIVESWITCH;
|
|
|
|
// Is player looking at inventory?
|
|
nicePlayer = NicePlayerController(Instigator.Controller);
|
|
if(nicePlayer == none || !bIsReloading)
|
|
return;
|
|
HUD = HUDKillingFloor(nicePlayer.MyHUD);
|
|
bDisplayInventory = HUD != none && HUD.bDisplayInventory;
|
|
|
|
// General checks: is there any meaning to reset reload for provided cause?
|
|
if(bCauseFire)
|
|
bWeReallyShould = !bNeedToCharge && (bDisplayInventory || magAmmoRemainingClient >= GetFireMode(0).ammoPerFire);
|
|
else if(bCauseAltFire)
|
|
bWeReallyShould = !bNeedToCharge && AltFireCanForceInterruptReload()
|
|
&& (reloadType == RTYPE_AUTO || !bAutoReload);
|
|
else if(bCauseNade){
|
|
bWeReallyShould = true;
|
|
nicePawn = NiceHumanPawn(nicePlayer.Pawn);
|
|
if(nicePawn == none)
|
|
bWeReallyShould = false;
|
|
else{
|
|
if(nicePawn.PlayerGrenade == none)
|
|
nicePawn.PlayerGrenade = nicePawn.FindPlayerGrenade();
|
|
if(nicePawn.PlayerGrenade == none || !nicePawn.PlayerGrenade.HasAmmo())
|
|
bWeReallyShould = false;
|
|
}
|
|
}
|
|
else if(bCauseAim)
|
|
bWeReallyShould = magAmmoRemainingClient >= GetFireMode(0).ammoPerFire && GetReloadStage() != RSTAGE_POSTREL;
|
|
else
|
|
bWeReallyShould = true;
|
|
|
|
// If this is a magazine type reload - check player's preferences
|
|
if(reloadType == RTYPE_MAG){
|
|
if(GetReloadStage() == RSTAGE_TRASH)
|
|
bWeReallyShould = true;
|
|
else if(bCauseFire || bCauseAltFire)
|
|
bWeReallyShould = bWeReallyShould && nicePlayer.bRelCancelByFire;
|
|
else if(bCauseSwitch)
|
|
bWeReallyShould = bWeReallyShould && nicePlayer.bRelCancelBySwitching;
|
|
else if(cause == CANCEL_NADE)
|
|
bWeReallyShould = bWeReallyShould && nicePlayer.bRelCancelByNades;
|
|
else
|
|
bWeReallyShould = bWeReallyShould && nicePlayer.bRelCancelByAiming;
|
|
}
|
|
|
|
// Allow interrupting auto reload (by pausing) to throw a grenade
|
|
if(bAutoReload){
|
|
if(cause == CANCEL_NADE && bWeReallyShould){
|
|
bAutoReloadPaused = true;
|
|
autoReloadPauseFrame = GetCurrentAnimFrame();
|
|
}
|
|
else if(currentAutoReloadStage == RAUTOSTAGE_UNINTERRUPTIBLE)
|
|
bWeReallyShould = false;
|
|
}
|
|
|
|
// Interrupt if we really should
|
|
if(bIsReloading && bWeReallyShould){
|
|
HideMagazine(bMagazineOut);
|
|
if(bAutoReload){
|
|
bActualAllowAutoReloadSkip = bAllowAutoReloadSkip;
|
|
if(KFGameType(Level.Game) != none)
|
|
bActualAllowAutoReloadSkip = bAllowAutoReloadSkip
|
|
|| KFGameType(Level.Game).WaveNum == KFGameType(Level.Game).FinalWave;
|
|
if(bActualAllowAutoReloadSkip && bCauseSwitch && !bAutoReloadPaused){
|
|
AddAutoReloadedAmmo();
|
|
bAutoReloadRateApplied = false;
|
|
}
|
|
else if(bCauseSwitch || bCauseNade)
|
|
bAutoReloadInterrupted = true;
|
|
}
|
|
ServerStopReload();
|
|
bIsReloading = false;
|
|
bAutoReload = false;
|
|
lastEventCheckFrame = -1.0;
|
|
currentAutoReloadStage = RAUTOSTAGE_NONE;
|
|
PlayIdle();
|
|
|
|
if(bCauseNade){
|
|
if(cause == CANCEL_NADE && KFHumanPawn(Instigator) != none)
|
|
ClientThrowGrenade();
|
|
else if(cause == CANCEL_COOKEDNADE && ScrnHumanPawn(Instigator) != none)
|
|
ClientCookGrenade();
|
|
}
|
|
else if(cause == CANCEL_SWITCH)
|
|
ClientPutDown();
|
|
}
|
|
}
|
|
|
|
// Indicates if alt. fire should also interrupt reload
|
|
simulated function bool AltFireCanForceInterruptReload(){
|
|
if(FireModeClass[1] != none && FireModeClass[1] != class'KFMod.NoFire' && (NiceFire(FireMode[1]) == none || !NiceFire(FireMode[1]).fireState.base.bDisabled))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// Auxiliary functions to force certain function on client-side
|
|
simulated function bool ClientPutDown(){
|
|
return PutDown();
|
|
}
|
|
simulated function ClientThrowGrenade(){
|
|
if(KFHumanPawn(Instigator) != none)
|
|
KFHumanPawn(Instigator).ThrowGrenade();
|
|
}
|
|
simulated function ClientCookGrenade(){
|
|
if(ScrnHumanPawn(Instigator) != none)
|
|
ScrnHumanPawn(Instigator).CookGrenade();
|
|
}
|
|
|
|
// Functions that we need to reload in order to allow reload interruption on certain actions
|
|
simulated function bool PutDown(){
|
|
if(NicePlayerController(Instigator.Controller) != none)
|
|
ClientForceInterruptReload(CANCEL_SWITCH);
|
|
if(!bIsReloading)
|
|
HideMagazine(bMagazineOut);
|
|
TurnOffLaser();
|
|
return super.PutDown();
|
|
}
|
|
simulated function Fire(float F){
|
|
if(NicePlayerController(Instigator.Controller) != none)
|
|
ClientForceInterruptReload(CANCEL_FIRE);
|
|
super.Fire(F);
|
|
}
|
|
simulated function AltFire(float F){
|
|
if(NicePlayerController(Instigator.Controller) != none)
|
|
ClientForceInterruptReload(CANCEL_ALTFIRE);
|
|
Super.AltFire(F);
|
|
// Also request auto reload on alt. fire
|
|
if(!bHasSecondaryAmmo && AltFireCanForceInterruptReload() && magAmmoRemainingClient <= 0)
|
|
ServerRequestAutoReload();
|
|
else if(bHasSecondaryAmmo && !bAutoReload && FireModeClass[1] != class'KFMod.NoFire' && secondaryCharge <= 0)
|
|
ResumeAutoReload(autoReloadsDescriptions[currentAutoReload].resumeFrame);
|
|
}
|
|
simulated exec function ToggleIronSights(){
|
|
if(NicePlayerController(Instigator.Controller) != none)
|
|
ClientForceInterruptReload(CANCEL_AIM);
|
|
Super.ToggleIronSights();
|
|
}
|
|
simulated exec function IronSightZoomIn(){
|
|
if(NicePlayerController(Instigator.Controller) != none)
|
|
ClientForceInterruptReload(CANCEL_AIM);
|
|
Super.IronSightZoomIn();
|
|
}
|
|
|
|
// Function for filling-up reload stages in a single reload
|
|
simulated function FillSubReloadStages(){}
|
|
|
|
simulated function UpdateSingleReloadVars(){
|
|
reloadPreEndFrame = 0.0;
|
|
if(reloadStages.Length > 0)
|
|
reloadEndFrame = reloadStages[reloadStages.Length - 1];
|
|
else
|
|
reloadEndFrame = 0.0;
|
|
reloadChargeStartFrame = -1.0;
|
|
reloadChargeEndFrame = -1.0;
|
|
bHasChargePhase = false;
|
|
}
|
|
|
|
// Function that setups all the variable for next reload; this DOES NOT start reload animation or reload itself
|
|
simulated function SetupReloadVars(optional bool bIsActive, optional int animationIndex){
|
|
if(Role == ROLE_Authority)
|
|
return;
|
|
bIsReloading = true;
|
|
bAutoReloadRateApplied = false;
|
|
bAutoReloadInterrupted = false;
|
|
bAutoReloadPaused = false;
|
|
currentAutoReloadStage = RAUTOSTAGE_UNINTERRUPTIBLE;
|
|
autoReloadPauseFrame = 0.0;
|
|
currentAutoReload = 0;
|
|
activeReloadState = ACTR_NONE;
|
|
if(bIsActive){
|
|
bAutoReload = true;
|
|
currentAutoReload = Clamp(animationIndex, 0, autoReloadsDescriptions.Length - 1);
|
|
}
|
|
}
|
|
|
|
// Reset all the necessary variables after reloading
|
|
simulated function ResetReloadVars(){
|
|
if(Role == ROLE_Authority)
|
|
return;
|
|
ServerStopReload();
|
|
bIsReloading = false;
|
|
bAutoReload = false;
|
|
bAutoReloadInterrupted = false;
|
|
currentAutoReloadStage = RAUTOSTAGE_NONE;
|
|
currentAutoReload = 0;
|
|
activeReloadState = ACTR_NONE;
|
|
quickHeadshots = 0;
|
|
ClientTryPendingWeapon();
|
|
}
|
|
|
|
// Does what 'SetAnimFrame' does + updates 'lastEventCheckFrame' for correct event handling
|
|
// Use this instead of 'SetAnimFrame', unless you have a very specific need and know what you're doing
|
|
simulated function ScrollAnim(float newFrame){
|
|
SetAnimFrame(newFrame);
|
|
lastEventCheckFrame = newFrame;
|
|
}
|
|
|
|
// Starts reload at given rate from given stage.
|
|
// If specified stage is either 'RSTAGE_NONE' or 'RSTAGE_PREREL', - start reload from the beginning
|
|
simulated function PlayReloadAnimation(float rate, ERelStage stage){
|
|
if(!HasAnim(ReloadAnim))
|
|
return;
|
|
PlayAnim(ReloadAnim, rate, 0.0);
|
|
if(stage != RSTAGE_NONE && stage != RSTAGE_PREREL){
|
|
if(stage == RSTAGE_MAINREL){
|
|
ScrollAnim(reloadMagStartFrame);
|
|
HideMagazine(false);
|
|
}
|
|
else if(stage == RSTAGE_POSTREL)
|
|
ScrollAnim(reloadChargeStartFrame);
|
|
else if(stage == RSTAGE_TRASH)
|
|
ScrollAnim(reloadChargeEndFrame);
|
|
}
|
|
}
|
|
|
|
// Resumes previously interrupted auto reload.
|
|
// Doesn't do any check for whether or not interruption took place
|
|
simulated function ResumeAutoReload(float startFrame){
|
|
local float ReloadMulti;
|
|
if(!HasAnim(autoReloadsDescriptions[currentAutoReload].animName))
|
|
return;
|
|
ReloadMulti = GetCurrentReloadMult();
|
|
PlayAnim(autoReloadsDescriptions[currentAutoReload].animName, AutoReloadBaseRate() * ReloadMulti, 0.0);
|
|
if(startFrame <= 0)
|
|
startFrame = 0.0;
|
|
ScrollAnim(startFrame);
|
|
ServerReload((1 - startFrame) * AutoReloadBaseRate() / ReloadMulti);
|
|
SetupReloadVars(true, currentAutoReload);
|
|
}
|
|
// Returns reload speed that weapon should have at the moment
|
|
simulated function float GetFittingReloadSpeed(){
|
|
return default.ReloadAnimRate * GetCurrentReloadMult();
|
|
}
|
|
// Updates current reload rate
|
|
simulated function UpdateReloadRate(){
|
|
lastRecordedReloadRate = GetFittingReloadSpeed();
|
|
if(bIsReloading)
|
|
ChangeReloadRate(lastRecordedReloadRate);
|
|
}
|
|
|
|
// Changes rate of current animation, allowing it to continue from the same frame
|
|
simulated function ChangeReloadRate(float newRate){
|
|
local name SeqName;
|
|
local float AnimFrame, AnimRate;
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(AnimFrame < 0)
|
|
AnimFrame = 0;
|
|
if(!bAutoReload && SeqName != ReloadAnim)
|
|
return;
|
|
if(bAutoReload)
|
|
PlayAnim(SeqName, newRate, 0.0);
|
|
else
|
|
PlayReloadAnimation(newRate, RSTAGE_NONE);
|
|
ScrollAnim(AnimFrame);
|
|
}
|
|
|
|
// New handler for client's reload request
|
|
// Can be called directly as a command or automatically, by intercepting ReloadMeNow command (handled in 'NiceInteraction')
|
|
exec simulated function ClientReloadMeNow(){
|
|
local float ReloadMulti;
|
|
local NicePlayerController nicePlayer;
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
nicePlayer = NicePlayerController(Instigator.Controller);
|
|
if(nicePlayer != none)
|
|
niceVet = class'NiceVeterancyTypes'.static.GetVeterancy(nicePlayer.PlayerReplicationInfo);
|
|
bGiveObsessiveBonus = false;
|
|
if(bIsReloading)
|
|
AttemptActiveReload();
|
|
else if(niceVet.static.hasSkill(nicePlayer, class'NiceSkillSupportObsessive') && GetMagazineAmmo() >= float(MagCapacity) * class'NiceSkillSupportObsessive'.default.reloadLevel)
|
|
bGiveObsessiveBonus = true;
|
|
if(!AllowReload())
|
|
return;
|
|
if(reloadType == RTYPE_AUTO)
|
|
ResumeAutoReload(autoReloadsDescriptions[currentAutoReload].resumeFrame);
|
|
else{
|
|
SetupReloadVars();
|
|
if(bHasAimingMode && bAimingRifle && !bAutoReload){
|
|
FireMode[1].bIsFiring = false;
|
|
ZoomOut(false);
|
|
ServerZoomOut(false);
|
|
}
|
|
subReloadStage = 0;
|
|
ReloadMulti = GetCurrentReloadMult();
|
|
if(bMagazineOut && reloadMagStartFrame >= 0){
|
|
ServerReload((1 - reloadMagStartFrame) * default.ReloadRate / ReloadMulti);
|
|
PlayReloadAnimation(default.ReloadAnimRate * ReloadMulti, RSTAGE_MAINREL);
|
|
}
|
|
else{
|
|
ServerReload(default.ReloadRate / ReloadMulti);
|
|
PlayReloadAnimation(default.ReloadAnimRate * ReloadMulti, RSTAGE_NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tells server that reload was started and how long should it take
|
|
function ServerReload(float duration, optional bool autoReload){
|
|
bIsReloading = true;
|
|
Instigator.SetAnimAction(WeaponReloadAnim);
|
|
ReloadDeadLine = Level.TimeSeconds + duration;
|
|
if(bHasAimingMode && bAimingRifle && !autoReload)
|
|
FireMode[1].bIsFiring = false;
|
|
if(Level.Game.NumPlayers > 1 && KFGameType(Level.Game).bWaveInProgress && KFPlayerController(Instigator.Controller) != none &&
|
|
Level.TimeSeconds - KFPlayerController(Instigator.Controller).LastReloadMessageTime > KFPlayerController(Instigator.Controller).ReloadMessageDelay){
|
|
KFPlayerController(Instigator.Controller).Speech('AUTO', 2, "");
|
|
KFPlayerController(Instigator.Controller).LastReloadMessageTime = Level.TimeSeconds;
|
|
}
|
|
}
|
|
|
|
// Shift deadline of reload on server
|
|
function ServerShiftReloadTime(float delta){
|
|
if(bIsReloading)
|
|
ReloadDeadLine += delta;
|
|
}
|
|
|
|
// Forces reload to stop on server
|
|
function ServerStopReload(){
|
|
bIsReloading = false;
|
|
}
|
|
|
|
// Reloaded to implement new reload mechanism
|
|
simulated function WeaponTick(float dt){
|
|
local int i;
|
|
local name SeqName;
|
|
local float bringupAnimDuration;
|
|
local float ReloadMulti;
|
|
local float AnimFrame, AnimRate;
|
|
local ERelStage newStage;
|
|
UpdateAppearance();
|
|
if(lastRecordedReloadRate != GetFittingReloadSpeed())
|
|
UpdateReloadRate();
|
|
|
|
if(Role < ROLE_Authority && variantSwapState != SWAP_NONE){
|
|
if(variantSwapCountdown > 0)
|
|
variantSwapCountdown -= dt;
|
|
if(variantSwapCountdown <= 0 && variantSwapState == SWAP_PUTDOWN){
|
|
SetVariant(pendingVariant);
|
|
UpdateAppearance();
|
|
bringupAnimDuration = GetAnimDuration(SelectAnim);
|
|
PlayAnim(SelectAnim, bringupAnimDuration / variantSwapTime);
|
|
variantSwapState = SWAP_BRINGUP;
|
|
variantSwapCountdown = variantSwapTime;
|
|
}
|
|
if(variantSwapCountdown <= 0 && variantSwapState == SWAP_BRINGUP){
|
|
variantSwapState = SWAP_NONE;
|
|
variantSwapCountdown = 0;
|
|
}
|
|
}
|
|
|
|
// Resume charging magazine weapon next time we can
|
|
if(bNeedToCharge && GetReloadStage() == RSTAGE_NONE){
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(SeqName == IdleAnim){
|
|
ReloadMulti = GetCurrentReloadMult();
|
|
if(Role == ROLE_Authority)
|
|
ServerReload((1 - reloadChargeStartFrame) * default.ReloadRate / ReloadMulti);
|
|
else{
|
|
SetupReloadVars();
|
|
PlayReloadAnimation(ReloadAnimRate * ReloadMulti, RSTAGE_POSTREL);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resume auto reload from pause next time we can
|
|
if(bAutoReloadPaused && Role < ROLE_Authority && ClientGrenadeState == GN_NONE){
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(SeqName == IdleAnim)
|
|
ResumeAutoReload(autoReloadPauseFrame);
|
|
}
|
|
|
|
// Resume reloading in case auto reload was interrupted as soon as possible
|
|
if(bAutoReloadInterrupted && Role < ROLE_Authority && ClientGrenadeState == GN_none){
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(SeqName == IdleAnim)
|
|
ResumeAutoReload(autoReloadsDescriptions[currentAutoReload].resumeFrame);
|
|
}
|
|
|
|
// Try and detect when animation that should trigger auto reload starts
|
|
if(autoReloadsDescriptions.Length > 0 && currentAutoReloadStage == RAUTOSTAGE_NONE && Role < ROLE_Authority){
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
for(i = 0;i < autoReloadsDescriptions.Length;i ++)
|
|
if(SeqName == autoReloadsDescriptions[i].animName && AnimFrame < autoReloadsDescriptions[i].trashStartFrame){
|
|
// Since animation is already running - all we need to do is to setup appropriate variables
|
|
SetupReloadVars(true, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Random TWI's code block appears!
|
|
if(bHasAimingMode && bForceLeaveIronsights)
|
|
if(!shouldLeaveIronsight())
|
|
bForceLeaveIronsights = false;
|
|
if(bHasAimingMode){
|
|
if(bForceLeaveIronsights){
|
|
if(bAimingRifle){
|
|
ZoomOut(true);
|
|
if(Role < ROLE_Authority)
|
|
ServerZoomOut(false);
|
|
}
|
|
bForceLeaveIronsights = false;
|
|
}
|
|
if(ForceZoomOutTime > 0){
|
|
if(bAimingRifle){
|
|
if(Level.TimeSeconds - ForceZoomOutTime > 0){
|
|
ForceZoomOutTime = 0;
|
|
ZoomOut(true);
|
|
if(Role < ROLE_Authority)
|
|
ServerZoomOut(false);
|
|
}
|
|
}
|
|
else
|
|
ForceZoomOutTime = 0;
|
|
}
|
|
}
|
|
|
|
// We want to be up to date on this one
|
|
UpdateMagCapacity(Instigator.PlayerReplicationInfo);
|
|
// Next we have 3 possibilities.
|
|
if(Role == ROLE_Authority){
|
|
// 1. We're on the server. Then just check if we've reached reload deadline.
|
|
if(Level.TimeSeconds >= ReloadDeadLine)
|
|
bIsReloading = false;
|
|
}
|
|
else{
|
|
if(bAutoReload)
|
|
// 2. It's auto reload. Handle it in it's own tick function.
|
|
AutoReloadTick();
|
|
else if(Role < ROLE_Authority){
|
|
// 3. It's not. Then just handle stage swapping for magazine ('goThroughStages' function) and single reloads ('goThroughSubStages' function)
|
|
newStage = GetReloadStage();
|
|
if(reloadType == RTYPE_SINGLE && newStage == RSTAGE_MAINREL)
|
|
goThroughSubStages();
|
|
if(currentRelStage != newStage)
|
|
goThroughStages(currentRelStage, newStage);
|
|
}
|
|
if(Role < ROLE_Authority){
|
|
// Call events
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(newStage == RSTAGE_NONE && !bAutoReload)
|
|
AnimFrame = 1.0;
|
|
if(newStage != RSTAGE_NONE || lastEventCheckFrame > 0 || bAutoReload)
|
|
HandleReloadEvents(lastEventCheckFrame, AnimFrame);
|
|
if(newStage == RSTAGE_NONE && !bAutoReload)
|
|
lastEventCheckFrame = -1.0;
|
|
}
|
|
}
|
|
|
|
// Some other TWI's code leftovers
|
|
if((Level.NetMode == NM_Client) || Instigator == none || KFFriendlyAI(Instigator.Controller) == none && Instigator.PlayerReplicationInfo == none)
|
|
return;
|
|
|
|
// Turn it off on death / battery expenditure
|
|
if(FlashLight != none){
|
|
// Keep the 1P weapon client beam up to date.
|
|
AdjustLightGraphic();
|
|
if(FlashLight.bHasLight){
|
|
if(Instigator.Health <= 0 || KFHumanPawn(Instigator).TorchBatteryLife <= 0 || Instigator.PendingWeapon != none ){
|
|
KFHumanPawn(Instigator).bTorchOn = false;
|
|
ServerSpawnLight();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
simulated function HandleReloadEvents(float oldFrame, float newFrame){
|
|
local int i;
|
|
local float currEventFrame;
|
|
|
|
// Old reload ended and new started between checks somehow, no point in it, but try to fix that
|
|
if(oldFrame > newFrame){
|
|
if(oldFrame < 1.0)
|
|
HandleReloadEvents(oldFrame, 1.0);
|
|
if(newFrame > 0.0)
|
|
HandleReloadEvents(0.0, newFrame);
|
|
return;
|
|
}
|
|
for(i = 0;i < relEvents.Length;i ++){
|
|
currEventFrame = relEvents[i].eventFrame;
|
|
if(oldFrame < currEventFrame && newFrame >= currEventFrame)
|
|
ReloadEvent(relEvents[i].eventName);
|
|
}
|
|
if(newFrame > lastEventCheckFrame)
|
|
lastEventCheckFrame = newFrame;
|
|
}
|
|
|
|
simulated function ReloadEvent(string eventName){}
|
|
|
|
// This function is called each tick while auto reload is active
|
|
simulated function AutoReloadTick(){
|
|
local bool bFinishAutoReload;
|
|
local name SeqName;
|
|
local float AnimFrame, AnimRate;
|
|
bFinishAutoReload = false;
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
// Apply reload rate to the animation's speed when it's time
|
|
if(AnimFrame > autoReloadsDescriptions[currentAutoReload].speedFrame && !bAutoReloadRateApplied){
|
|
ChangeReloadRate(AutoReloadBaseRate() * GetCurrentReloadMult());
|
|
bAutoReloadRateApplied = true;
|
|
}
|
|
// Change state to interruptible and set reload to finish if there's no ammo
|
|
if(AnimFrame > autoReloadsDescriptions[currentAutoReload].canInterruptFrame){
|
|
if(autoReloadAmmo() > 0){
|
|
currentAutoReloadStage = RAUTOSTAGE_INTERRUPTIBLE;
|
|
ClientTryPendingWeapon();
|
|
}
|
|
else{
|
|
bFinishAutoReload = true;
|
|
PlayIdle();
|
|
}
|
|
}
|
|
// Might as well end reload when we enter trash stage (or when animation is finished)
|
|
if(SeqName != autoReloadsDescriptions[currentAutoReload].animName || AnimFrame > autoReloadsDescriptions[currentAutoReload].trashStartFrame){
|
|
bFinishAutoReload = true;
|
|
AddAutoReloadedAmmo();
|
|
}
|
|
// Finish the reload as asked
|
|
if(bFinishAutoReload)
|
|
ResetReloadVars();
|
|
}
|
|
|
|
simulated function float AutoReloadBaseRate(){
|
|
if(reloadType == RTYPE_AUTO && FireModeClass[0] != none
|
|
&& (!bHasSecondaryAmmo || FireMode[1] != Class'KFMod.NoFire'))
|
|
return FireModeClass[0].default.FireAnimRate;
|
|
return FireModeClass[1].default.FireAnimRate;
|
|
}
|
|
|
|
// Called when current state of reload changes; only called on client side
|
|
simulated function ReloadChangedStage(ERelStage prevStage, ERelStage newStage){
|
|
local NiceHumanPawn nicePawn;
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
nicePawn = NiceHumanPawn(Instigator);
|
|
if(nicePawn != none)
|
|
niceVet = class'NiceVeterancyTypes'.static.GetVeterancy(nicePawn.PlayerReplicationInfo);
|
|
// Reload was canceled, so all that is meaningless
|
|
if(!bIsReloading)
|
|
return;
|
|
if(reloadType == RTYPE_MAG){
|
|
if(newStage == RSTAGE_MAINREL){
|
|
bNeedToCharge = false;
|
|
ServerSetCharging(bNeedToCharge);
|
|
bMagazineOut = true;
|
|
MagAmmoRemainingClient = 0;
|
|
if(bHasChargePhase && bRoundInChamber)
|
|
MagAmmoRemainingClient ++;
|
|
ServerSetMagSize(MagAmmoRemainingClient, bRoundInChamber, Level.TimeSeconds);
|
|
}
|
|
else if(newStage == RSTAGE_POSTREL){
|
|
bMagazineOut = false;
|
|
HideMagazine(false);
|
|
if(bHasChargePhase){
|
|
if(bRoundInChamber){
|
|
bNeedToCharge = false;
|
|
PlayIdle();
|
|
}
|
|
else
|
|
bNeedToCharge = true;
|
|
}
|
|
ServerSetCharging(bNeedToCharge);
|
|
}
|
|
else if(newStage == RSTAGE_TRASH && (!bHasChargePhase || bRoundInChamber)){
|
|
bMagazineOut = false;
|
|
HideMagazine(false);
|
|
}
|
|
else if(newStage == RSTAGE_NONE)
|
|
ResetReloadVars();
|
|
if(prevStage == RSTAGE_MAINREL && newStage != RSTAGE_NONE)
|
|
AddReloadedAmmo();
|
|
if(prevStage == RSTAGE_POSTREL && newStage == RSTAGE_TRASH){
|
|
bNeedToCharge = false;
|
|
bRoundInChamber = true;
|
|
AmmoStackPush(ammoState[0], ammoState[0].currentAmmoType);
|
|
ServerSetCharging(bNeedToCharge);
|
|
ServerSetMagSize(MagAmmoRemainingClient, bRoundInChamber, Level.TimeSeconds);
|
|
}
|
|
if(newStage == RSTAGE_TRASH)
|
|
ClientTryPendingWeapon();
|
|
}
|
|
else if(reloadType == RTYPE_SINGLE){
|
|
if(newStage == RSTAGE_NONE)
|
|
ResetReloadVars();
|
|
if(prevStage == RSTAGE_MAINREL && bIsReloading)
|
|
goThroughSubStages(true);
|
|
}
|
|
}
|
|
|
|
// Return current magazine (also has limited use for single reload) stage of reload
|
|
simulated function ERelStage GetReloadStage(){
|
|
local name SeqName;
|
|
local float AnimFrame, AnimRate;
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(SeqName != ReloadAnim)
|
|
return RSTAGE_NONE;
|
|
if(AnimFrame < reloadPreEndFrame)
|
|
return RSTAGE_PREREL;
|
|
if(AnimFrame < reloadEndFrame)
|
|
return RSTAGE_MAINREL;
|
|
if(AnimFrame < reloadChargeEndFrame && bHasChargePhase)
|
|
return RSTAGE_POSTREL;
|
|
return RSTAGE_TRASH;
|
|
}
|
|
|
|
// Returns next magazine reload stage
|
|
simulated function ERelStage GetNextReloadStage(ERelStage curr){
|
|
local byte i;
|
|
i = curr;
|
|
i ++;
|
|
curr = ERelStage(i);
|
|
if(curr == RSTAGE_POSTREL && !bHasChargePhase)
|
|
curr = RSTAGE_TRASH;
|
|
return curr;
|
|
}
|
|
|
|
// Function that goes between given 'prev' and 'next' stages by passing every intermediate stage and calling 'ReloadChangedStage'
|
|
simulated function GoThroughStages(ERelStage prev, ERelStage next){
|
|
local ERelStage theEnum, limitStage;
|
|
|
|
if(prev < next || next == RSTAGE_NONE){
|
|
theEnum = prev;
|
|
if(next == RSTAGE_NONE)
|
|
limitStage = RSTAGE_TRASH;
|
|
else
|
|
limitStage = next;
|
|
while(theEnum < limitStage){
|
|
theEnum = GetNextReloadStage(theEnum);
|
|
ReloadChangedStage(currentRelStage, theEnum);
|
|
currentRelStage = theEnum;
|
|
}
|
|
}
|
|
if(prev > next){
|
|
ReloadChangedStage(prev, next);
|
|
currentRelStage = next;
|
|
}
|
|
}
|
|
|
|
simulated function GoThroughSubStages(optional bool bReloadEnded){
|
|
local float AnimFrame;
|
|
AnimFrame = GetCurrentAnimFrame();
|
|
// Conditions: 1. Is this valid stage?
|
|
// 2, 3. Can we even load more ammo?
|
|
while(subReloadStage < reloadStages.Length && MagAmmoRemainingClient < MagCapacity && AmmoAmount(0) - MagAmmoRemainingClient > 0)
|
|
if(bReloadEnded || AnimFrame > reloadStages[subReloadStage]){
|
|
AddReloadedAmmo();
|
|
subReloadStage ++;
|
|
}
|
|
else break;
|
|
// If reload hasn't ended, we can only load one more shell, but aren't yet at animation's end - scroll animation
|
|
// 'subReloadStage' shouldn't be zero at this point, but just in case check
|
|
// During reload client dictates size of the magazine
|
|
if(subReloadStage > 0 && !bReloadEnded && subReloadStage != reloadStages.Length && (MagAmmoRemainingClient >= MagCapacity || MagAmmoRemainingClient - AmmoAmount(0) >= 0)){
|
|
if(alwaysPlayAnimEnd){
|
|
AnimFrame = reloadStages[reloadStages.Length - 1] + AnimFrame - reloadStages[subReloadStage - 1];
|
|
ScrollAnim(AnimFrame); // Current animation position - previous ammo load position
|
|
}
|
|
else
|
|
PlayIdle();
|
|
subReloadStage = reloadStages.Length - 1;
|
|
}
|
|
}
|
|
|
|
//Auzilary unction for easy initial generation of sub reload stages for single reload
|
|
simulated function GenerateReloadStages(int stagesAmount, int framesAmount, int firstLoadFrame, int loadDelta){
|
|
local int i;
|
|
local int frame;
|
|
local float convFrame;
|
|
reloadStages.Length = 0;
|
|
frame = firstLoadFrame;
|
|
for(i = 0;i < stagesAmount;i ++){
|
|
// Next load time
|
|
convFrame = float(frame) / float(framesAmount);
|
|
reloadStages[reloadStages.Length] = convFrame;
|
|
// Shift to the next one
|
|
frame += loadDelta;
|
|
}
|
|
}
|
|
|
|
simulated function HideMagazine(bool bHide){
|
|
if(magazineBone == '')
|
|
return;
|
|
if(bHide)
|
|
SetBoneScale(0, 0.0, magazineBone);
|
|
else
|
|
SetBoneScale(0, 1.0, magazineBone);
|
|
}
|
|
|
|
simulated function float GetCurrentAnimFrame(){
|
|
local name SeqName;
|
|
local float AnimFrame, AnimRate;
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
return AnimFrame;
|
|
}
|
|
|
|
simulated function BringUp(optional Weapon PrevWeapon){
|
|
// Change HUD icons if necessary
|
|
if(Role < ROLE_Authority){
|
|
if(bChangeClipIcon && hudClipTexture != none)
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).ClipsIcon.WidgetTexture = hudClipTexture;
|
|
else
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).ClipsIcon.WidgetTexture =
|
|
class'ScrnHUD'.default.ClipsIcon.WidgetTexture;
|
|
if(bChangeBulletsIcon && hudBulletsTexture != none)
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).BulletsInClipIcon.WidgetTexture =
|
|
hudBulletsTexture;
|
|
else
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).BulletsInClipIcon.WidgetTexture =
|
|
class'ScrnHUD'.default.BulletsInClipIcon.WidgetTexture;
|
|
if(bChangeSecondaryIcon && hudSecondaryTexture != none)
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).SecondaryClipsIcon.WidgetTexture =
|
|
hudSecondaryTexture;
|
|
else
|
|
HUDKillingFloor(NicePlayerController(instigator.controller).myHUD).SecondaryClipsIcon.WidgetTexture =
|
|
class'ScrnHUD'.default.SecondaryClipsIcon.WidgetTexture;
|
|
}
|
|
HideMagazine(bMagazineOut);
|
|
super.BringUp(PrevWeapon);
|
|
ApplyLaserState();
|
|
}
|
|
|
|
function ServerSetCharging(bool bNewNeedToCharge){
|
|
bNeedToCharge = bNewNeedToCharge;
|
|
}
|
|
|
|
// Function that's supposed to return current amount of ammo that's used for auto reload for this weapon
|
|
simulated function int AutoReloadAmmo(){
|
|
if(FireModeClass[1] == class'KFMod.NoFire')
|
|
return AmmoAmount(0);
|
|
else
|
|
return AmmoAmount(1);
|
|
}
|
|
|
|
simulated function int GetMagazineAmmo(){
|
|
if(Role < ROLE_Authority)
|
|
return MagAmmoRemainingClient;
|
|
else
|
|
return MagAmmoRemaining;
|
|
}
|
|
|
|
simulated function bool AllowReload(){
|
|
local int actualMagSize;
|
|
actualMagSize = GetMagazineAmmo();
|
|
if(bHasChargePhase && bRoundInChamber)
|
|
actualMagSize --;
|
|
UpdateMagCapacity(Instigator.PlayerReplicationInfo);
|
|
if(FireMode[0].IsFiring() || FireMode[1].IsFiring() ||
|
|
bIsReloading || actualMagSize >= MagCapacity ||
|
|
ClientState == WS_BringUp ||
|
|
AmmoAmount(0) <= GetMagazineAmmo())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
simulated function bool IsMagazineFull(){
|
|
local int actualMagSize;
|
|
actualMagSize = GetMagazineAmmo();
|
|
if(bHasChargePhase && bRoundInChamber)
|
|
actualMagSize --;
|
|
return (actualMagSize >= MagCapacity || actualMagSize >= AmmoAmount(0));
|
|
}
|
|
|
|
exec function ReloadMeNow(){
|
|
local NicePlayerController nicePlayer;
|
|
nicePlayer = NicePlayerController(Instigator.Controller);
|
|
if(nicePlayer != none && nicePlayer.bFlagUseServerReload)
|
|
ClientReloadMeNow();
|
|
}
|
|
|
|
simulated function float GetCurrentReloadMult(){
|
|
local float ReloadMulti;
|
|
local NiceHumanPawn nicePawn;
|
|
local NicePlayerController nicePlayer;
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
nicePawn = NiceHumanPawn(Instigator);
|
|
nicePlayer = NicePlayerController(Instigator.Controller);
|
|
if(nicePawn != none)
|
|
niceVet = class'NiceVeterancyTypes'.static.GetVeterancy(nicePawn.PlayerReplicationInfo);
|
|
if(KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo) != none && KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo).ClientVeteranSkill != none)
|
|
ReloadMulti = KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo).ClientVeteranSkill.Static.GetReloadSpeedModifier(KFPlayerReplicationInfo(Instigator.PlayerReplicationInfo), self);
|
|
else
|
|
ReloadMulti = 1.0;
|
|
if(bGiveObsessiveBonus)
|
|
ReloadMulti *= class'NiceSkillSupportObsessive'.default.reloadBonus;
|
|
// Active reload speedup
|
|
if(activeReloadState == ACTR_SUCCESS)
|
|
ReloadMulti *= activeSpeedup;
|
|
else if(activeReloadState == ACTR_FAIL)
|
|
ReloadMulti *= activeSlowdown;
|
|
else if(bCanActiveReload && reloadType == RTYPE_SINGLE && subReloadStage == 0)
|
|
ReloadMulti *= activeSpeedup;
|
|
if(nicePlayer != none && niceVet.static.hasSkill(nicePlayer, class'NiceSkillCommandoZEDProfessional'))
|
|
ReloadMulti /= (Level.TimeDilation / 1.1);
|
|
if(bAutoReload && bAutoReloadRateApplied)
|
|
ReloadMulti *= autoReloadSpeedModifier;
|
|
return ReloadMulti;
|
|
}
|
|
|
|
function ServerRequestAutoReload(){
|
|
ClientReloadMeNow();
|
|
}
|
|
|
|
simulated function AttemptActiveReload(optional bool bForce){
|
|
local float windowStart;
|
|
local float windowLenght;
|
|
local name SeqName;
|
|
local float AnimFrame, AnimRate;
|
|
local NiceHumanPawn nicePawn;
|
|
local class<NiceVeterancyTypes> niceVet;
|
|
// Need this for skill check
|
|
nicePawn = NiceHumanPawn(Instigator);
|
|
if(nicePawn != none)
|
|
niceVet = class'NiceVeterancyTypes'.static.GetVeterancy(nicePawn.PlayerReplicationInfo);
|
|
// Does nothing if we aren't even reloading, this is auto reload, we've already succeeded/failed or we'll auto succeed anyway
|
|
if(!bIsReloading || !bCanActiveReload || bAutoReload || activeReloadState == ACTR_SUCCESS || activeReloadState == ACTR_FAIL)
|
|
return;
|
|
// Find starting frame and length of the active reload window (and declare fail if single reload is still in the first sub-stage)
|
|
windowStart = -1.0;
|
|
if(reloadType == RTYPE_MAG){
|
|
windowStart = reloadPreEndFrame;
|
|
windowLenght = activeWindow;
|
|
}
|
|
else if(reloadType == RTYPE_SINGLE){
|
|
// Too early!
|
|
if(subReloadStage <= 0){
|
|
activeReloadState = ACTR_FAIL;
|
|
UpdateReloadRate();
|
|
return;
|
|
}
|
|
windowStart = reloadStages[subReloadStage - 1];
|
|
windowLenght = activeWindow / MagCapacity;
|
|
}
|
|
// Something went wrong and active reload is inapplicable
|
|
if(windowStart < 0)
|
|
return;
|
|
GetAnimParams(0, SeqName, AnimFrame, AnimRate);
|
|
if(windowStart <= AnimFrame && AnimFrame <= windowStart + windowLenght || bForce)
|
|
activeReloadState = ACTR_SUCCESS;
|
|
else
|
|
activeReloadState = ACTR_FAIL;
|
|
UpdateReloadRate();
|
|
}
|
|
|
|
// Function that's called when client tries to use flashlight on a weapon
|
|
simulated function SecondDoToggle(){}
|
|
|
|
simulated function ClientReload(){}
|
|
|
|
simulated function bool InterruptReload(){
|
|
return false;
|
|
}
|
|
|
|
function ServerStopFire(byte Mode){
|
|
super(BaseKFWeapon).ServerStopFire(Mode);
|
|
}
|
|
|
|
simulated function ClientTryPendingWeapon(){
|
|
if(Instigator.PendingWeapon != none && Instigator.PendingWeapon != self)
|
|
Instigator.Controller.ClientSwitchToBestWeapon();
|
|
}
|
|
|
|
simulated function AnimEnd(int channel){
|
|
local name anim;
|
|
local float frame, rate;
|
|
|
|
GetAnimParams(0, anim, frame, rate);
|
|
|
|
if(!FireMode[0].IsInState('FireLoop')){
|
|
GetAnimParams(0, anim, frame, rate);
|
|
if(ClientState == WS_ReadyToFire)
|
|
if((FireMode[0] == none || !FireMode[0].bIsFiring) && (FireMode[1] == none || !FireMode[1].bIsFiring))
|
|
PlayIdle();
|
|
}
|
|
else if(ClientState == WS_ReadyToFire){
|
|
if(anim == FireMode[0].FireAnim && HasAnim(FireMode[0].FireEndAnim))
|
|
PlayAnim(FireMode[0].FireEndAnim, FireMode[0].FireEndAnimRate, 0.0);
|
|
else if (anim== FireMode[1].FireAnim && HasAnim(FireMode[1].FireEndAnim))
|
|
PlayAnim(FireMode[1].FireEndAnim, FireMode[1].FireEndAnimRate, 0.0);
|
|
else if ((FireMode[0] == none || !FireMode[0].bIsFiring) && (FireMode[1] == none || !FireMode[1].bIsFiring) && !bAutoReloadPaused)
|
|
PlayIdle();
|
|
}
|
|
}
|
|
|
|
simulated function bool StartFire(int Mode){
|
|
/*if(NiceHighROFFire(FireMode[Mode]) == none || FireMode[Mode].bWaitForRelease)
|
|
return super.StartFire(Mode);*/ //MEANTODO
|
|
|
|
if(!super.StartFire(Mode))
|
|
return false;
|
|
|
|
if(AmmoAmount(0) <= 0)
|
|
return false;
|
|
|
|
AnimStopLooping();
|
|
|
|
if(!FireMode[Mode].IsInState('FireLoop') && (AmmoAmount(0) > 0)){
|
|
FireMode[Mode].StartFiring();
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
simulated event OnZoomOutFinished(){
|
|
local name anim;
|
|
local float frame, rate;
|
|
|
|
GetAnimParams(0, anim, frame, rate);
|
|
if(!FireMode[0].IsInState('FireLoop'))
|
|
super.OnZoomOutFinished();
|
|
else if(ClientState == WS_ReadyToFire){
|
|
// Play the regular idle anim when we're finished zooming out
|
|
if(anim == IdleAimAnim)
|
|
PlayIdle();
|
|
// Switch looping fire anims if we switched to/from zoomed
|
|
else if( FireMode[0].IsInState('FireLoop') && anim == 'Fire_Iron_Loop')
|
|
LoopAnim('Fire_Loop', FireMode[0].FireLoopAnimRate, FireMode[0].TweenTime);
|
|
}
|
|
}
|
|
|
|
simulated event OnZoomInFinished(){
|
|
local name anim;
|
|
local float frame, rate;
|
|
|
|
GetAnimParams(0, anim, frame, rate);
|
|
|
|
if(!FireMode[0].IsInState('FireLoop'))
|
|
super.OnZoomInFinished();
|
|
else if(ClientState == WS_ReadyToFire){
|
|
// Play the iron idle anim when we're finished zooming in
|
|
if(anim == IdleAnim)
|
|
PlayIdle();
|
|
// Switch looping fire anims if we switched to/from zoomed
|
|
else if( FireMode[0].IsInState('FireLoop') && anim == 'Fire_Loop' )
|
|
LoopAnim('Fire_Iron_Loop', FireMode[0].FireLoopAnimRate, FireMode[0].TweenTime);
|
|
}
|
|
}
|
|
|
|
// Some functions reloaded to force update of magazine size on client's side
|
|
function GiveAmmo(int m, WeaponPickup WP, bool bJustSpawned){
|
|
super.GiveAmmo(m, WP, bJustSpawned);
|
|
ClientSetMagSize(MagAmmoRemaining, bRoundInChamber);
|
|
}
|
|
|
|
simulated function GiveTo(Pawn other, optional Pickup Pickup){
|
|
local int actualMagSize;
|
|
local NiceWeaponPickup niceWeapPickup;
|
|
local NicePlainData.Data dummyData;
|
|
niceWeapPickup = NiceWeaponPickup(Pickup);
|
|
if(niceWeapPickup != none)
|
|
SetNiceData(niceWeapPickup.GetNiceData(), NiceHumanPawn(other));
|
|
else
|
|
SetNiceData(dummyData, NiceHumanPawn(other));
|
|
if(Role == ROLE_Authority){
|
|
UpdateMagCapacity(other.PlayerReplicationInfo);
|
|
|
|
if(NiceWeaponPickup(Pickup) != none)
|
|
actualMagSize = NiceWeaponPickup(Pickup).MagAmmoRemaining;
|
|
if(bRoundInChamber && actualMagSize > 0)
|
|
actualMagSize --;
|
|
if(NiceWeaponPickup(Pickup) != none && Pickup.bDropped)
|
|
actualMagSize = Clamp(actualMagSize, 0, MagCapacity);
|
|
else
|
|
actualMagSize = MagCapacity;
|
|
MagAmmoRemaining = actualMagSize;
|
|
if(bRoundInChamber)
|
|
MagAmmoRemaining ++;
|
|
super(BaseKFWeapon).GiveTo(other, Pickup);
|
|
ClientSetMagSize(MagAmmoRemaining, bRoundInChamber);
|
|
}
|
|
}
|
|
|
|
function NicePlainData.Data GetNiceData(){
|
|
local NicePlainData.Data transferData;
|
|
if(LaserType > 0)
|
|
class'NicePlainData'.static.SetInt(transferData, "LaserType", int(LaserType));
|
|
class'NicePlainData'.static.SetBool(transferData, "ChamberedRound", bRoundInChamber);
|
|
class'NicePlainData'.static.SetFloat(transferData, "ArdourTimeout", ardourTimeout);
|
|
class'NicePlainData'.static.SetInt(transferData, "ChargeAmount", secondaryCharge);
|
|
return transferData;
|
|
}
|
|
function SetNiceData(NicePlainData.Data transferData, optional NiceHumanPawn newOwner){
|
|
local int newLaserType;
|
|
newLaserType = class'NicePlainData'.static.GetInt(transferData, "LaserType", -1);
|
|
if(newLaserType >= 0)
|
|
ClientSetLaserType(byte(newLaserType));// NICETODO: update round in chamber storing
|
|
bRoundInChamber = class'NicePlainData'.static.GetBool(transferData, "ChamberedRound", false);
|
|
ardourTimeout = class'NicePlainData'.static.GetFloat(transferData, "ArdourTimeout", 0.0);
|
|
secondaryCharge = class'NicePlainData'.static.GetInt(transferData, "ChargeAmount", 1);
|
|
ClientSetSndCharge(secondaryCharge);
|
|
}
|
|
|
|
simulated function ApplyLaserState(){
|
|
bLaserActive = LaserType > 0;
|
|
if(Role < ROLE_Authority)
|
|
ServerSetLaserType(LaserType);
|
|
|
|
if(NiceAttachment(ThirdPersonActor) != none)
|
|
NiceAttachment(ThirdPersonActor).SetLaserType(LaserType);
|
|
|
|
if(!Instigator.IsLocallyControlled())
|
|
return;
|
|
|
|
if(bLaserActive){
|
|
if(LaserDot == none)
|
|
LaserDot = Spawn(LaserDotClass, self);
|
|
LaserDot.SetLaserType(LaserType);
|
|
if(altLaserAttachmentBone != ''){
|
|
if(altLaserDot == none)
|
|
altLaserDot = Spawn(LaserDotClass, self);
|
|
altLaserDot.SetLaserType(LaserType);
|
|
}
|
|
//spawn 1-st person laser attachment for weapon owner
|
|
if(LaserAttachment == none){
|
|
SetBoneRotation(LaserAttachmentBone, LaserAttachmentRotation);
|
|
LaserAttachment = Spawn(LaserAttachmentClass,,,,);
|
|
AttachToBone(LaserAttachment, LaserAttachmentBone);
|
|
if(LaserAttachment != none)
|
|
LaserAttachment.SetRelativeLocation(LaserAttachmentOffset);
|
|
}
|
|
if(altLaserAttachment == none && altLaserAttachmentBone != ''){
|
|
SetBoneRotation(altLaserAttachmentBone, altLaserAttachmentRotation);
|
|
altLaserAttachment = Spawn(LaserAttachmentClass,,,,);
|
|
AttachToBone(altLaserAttachment, altLaserAttachmentBone);
|
|
if(altLaserAttachment != none)
|
|
altLaserAttachment.SetRelativeLocation(altLaserAttachmentOffset);
|
|
}
|
|
ConstantColor'ScrnTex.Laser.LaserColor'.Color = LaserDot.GetLaserColor();
|
|
LaserAttachment.bHidden = false;
|
|
altLaserAttachment.bHidden = false;
|
|
}
|
|
else{
|
|
if(LaserAttachment != none)
|
|
LaserAttachment.bHidden = true;
|
|
if(altLaserAttachment != none)
|
|
altLaserAttachment.bHidden = true;
|
|
if(LaserDot != none)
|
|
LaserDot.Destroy();
|
|
if(altLaserDot != none)
|
|
altLaserDot.Destroy();
|
|
}
|
|
}
|
|
|
|
simulated function ToggleLaser(){
|
|
if(!Instigator.IsLocallyControlled())
|
|
return;
|
|
// Will redo this bit later, but so far it'll have to do
|
|
if(LaserType == 0)
|
|
LaserType = 1;
|
|
else if(LaserType == 1)
|
|
LaserType = 4;
|
|
else if(LaserType == 4)
|
|
LaserType = 2;
|
|
else
|
|
LaserType = 0;
|
|
ApplyLaserState();
|
|
}
|
|
|
|
simulated function TurnOffLaser(){
|
|
if(!Instigator.IsLocallyControlled())
|
|
return;
|
|
|
|
if(Role < ROLE_Authority)
|
|
ServerSetLaserType(0);
|
|
|
|
bLaserActive = false;
|
|
if(LaserAttachment != none)
|
|
LaserAttachment.bHidden = true;
|
|
if(altLaserAttachment != none)
|
|
altLaserAttachment.bHidden = true;
|
|
if(LaserDot != none)
|
|
LaserDot.Destroy();
|
|
if(altLaserDot != none)
|
|
altLaserDot.Destroy();
|
|
}
|
|
|
|
function ServerSetLaserType(byte NewLaserType){
|
|
LaserType = NewLaserType;
|
|
bLaserActive = NewLaserType > 0;
|
|
if(NiceAttachment(ThirdPersonActor) != none)
|
|
NiceAttachment(ThirdPersonActor).SetLaserType(LaserType);
|
|
}
|
|
|
|
simulated function ClientSetLaserType(byte NewLaserType){
|
|
LaserType = NewLaserType;
|
|
bLaserActive = NewLaserType > 0;
|
|
ApplyLaserState();
|
|
}
|
|
|
|
simulated function NiceFire GetMainFire(){
|
|
return NiceFire(FireMode[0]);
|
|
}
|
|
|
|
function ServerSetSndCharge(int newCharge){
|
|
secondaryCharge = newCharge;
|
|
}
|
|
|
|
simulated function ClientSetSndCharge(int newCharge){
|
|
secondaryCharge = newCharge;
|
|
}
|
|
|
|
simulated function RenderOverlays(Canvas Canvas){
|
|
local int i;
|
|
local Vector StartTrace, EndTrace;
|
|
local Vector HitLocation, HitNormal;
|
|
local Actor Other;
|
|
local vector X,Y,Z;
|
|
local coords C;
|
|
local array<Actor> HitActors;
|
|
|
|
if(Instigator == none)
|
|
return;
|
|
|
|
if(Instigator.Controller != none)
|
|
Hand = Instigator.Controller.Handedness;
|
|
|
|
if((Hand < -1.0) || (Hand > 1.0))
|
|
return;
|
|
|
|
for(i = 0; i < NUM_FIRE_MODES;++ i)
|
|
if(FireMode[i] != none)
|
|
FireMode[i].DrawMuzzleFlash(Canvas);
|
|
|
|
SetLocation(Instigator.Location + Instigator.CalcDrawOffset(self));
|
|
SetRotation(Instigator.GetViewRotation() + ZoomRotInterp);
|
|
|
|
// Handle drawing the laser dots
|
|
if(LaserDot != none){
|
|
if(bIsReloading || bAllowFreeDot){
|
|
C = GetBoneCoords(LaserAttachmentBone);
|
|
X = C.XAxis;
|
|
Y = C.YAxis;
|
|
Z = C.ZAxis;
|
|
}
|
|
else
|
|
GetViewAxes(X, Y, Z);
|
|
|
|
StartTrace = Instigator.Location + Instigator.EyePosition();
|
|
EndTrace = StartTrace + 65535 * X;
|
|
|
|
while(true){
|
|
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
|
|
if(ROBulletWhipAttachment(Other) != none){
|
|
HitActors[HitActors.Length] = Other;
|
|
Other.SetCollision(false);
|
|
StartTrace = HitLocation + X;
|
|
}
|
|
else{
|
|
if(other != none && Other != Instigator && Other.Base != Instigator)
|
|
EndBeamEffect = HitLocation;
|
|
else
|
|
EndBeamEffect = EndTrace;
|
|
break;
|
|
}
|
|
}
|
|
// restore collision
|
|
for(i = 0; i<HitActors.Length;++ i)
|
|
HitActors[i].SetCollision(true);
|
|
|
|
LaserDot.SetLocation(EndBeamEffect - X*LaserDot.ProjectorPullback);
|
|
|
|
if(Pawn(Other) != none){
|
|
LaserDot.SetRotation(Rotator(X));
|
|
LaserDot.SetDrawScale(LaserDot.default.DrawScale * 0.5);
|
|
}
|
|
else if(HitNormal == vect(0,0,0)){
|
|
LaserDot.SetRotation(Rotator(-X));
|
|
LaserDot.SetDrawScale(LaserDot.default.DrawScale);
|
|
}
|
|
else{
|
|
LaserDot.SetRotation(Rotator(-HitNormal));
|
|
LaserDot.SetDrawScale(LaserDot.default.DrawScale);
|
|
}
|
|
}
|
|
if(altLaserDot != none){
|
|
if(bIsReloading || bAllowFreeDot){
|
|
C = GetBoneCoords(altLaserAttachmentBone);
|
|
X = C.XAxis;
|
|
Y = C.YAxis;
|
|
Z = C.ZAxis;
|
|
}
|
|
else
|
|
GetViewAxes(X, Y, Z);
|
|
|
|
StartTrace = Instigator.Location + Instigator.EyePosition();
|
|
EndTrace = StartTrace + 65535 * X;
|
|
|
|
while(true){
|
|
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
|
|
if(ROBulletWhipAttachment(Other) != none){
|
|
HitActors[HitActors.Length] = Other;
|
|
Other.SetCollision(false);
|
|
StartTrace = HitLocation + X;
|
|
}
|
|
else{
|
|
if(other != none && Other != Instigator && Other.Base != Instigator)
|
|
EndBeamEffect = HitLocation;
|
|
else
|
|
EndBeamEffect = EndTrace;
|
|
break;
|
|
}
|
|
}
|
|
// restore collision
|
|
for(i = 0; i<HitActors.Length;++ i)
|
|
HitActors[i].SetCollision(true);
|
|
|
|
altLaserDot.SetLocation(EndBeamEffect - X*altLaserDot.ProjectorPullback);
|
|
|
|
if(Pawn(Other) != none){
|
|
altLaserDot.SetRotation(Rotator(X));
|
|
altLaserDot.SetDrawScale(altLaserDot.default.DrawScale * 0.5);
|
|
}
|
|
else if(HitNormal == vect(0,0,0)){
|
|
altLaserDot.SetRotation(Rotator(-X));
|
|
altLaserDot.SetDrawScale(altLaserDot.default.DrawScale);
|
|
}
|
|
else{
|
|
altLaserDot.SetRotation(Rotator(-HitNormal));
|
|
altLaserDot.SetDrawScale(altLaserDot.default.DrawScale);
|
|
}
|
|
}
|
|
bDrawingFirstPerson = true;
|
|
Canvas.DrawActor(self, false, false, DisplayFOV);
|
|
bDrawingFirstPerson = false;
|
|
}
|
|
|
|
simulated function ZoomIn(bool bAnimateTransition){
|
|
default.ZoomTime = default.recordedZoomTime;
|
|
PlayerIronSightFOV = default.PlayerIronSightFOV;
|
|
if(class'NiceVeterancyTypes'.static.hasSkill(NicePlayerController(Instigator.Controller), class'NiceSkillSharpshooterHardWork')){
|
|
default.ZoomTime *= class'NiceSkillSharpshooterHardWork'.default.zoomSpeedBonus;
|
|
PlayerIronSightFOV *= class'NiceSkillSharpshooterHardWork'.default.zoomBonus;
|
|
}
|
|
super.ZoomIn(bAnimateTransition);
|
|
}
|
|
|
|
simulated function ZoomOut(bool bAnimateTransition){
|
|
default.ZoomTime = default.recordedZoomTime;
|
|
PlayerIronSightFOV = default.PlayerIronSightFOV;
|
|
if(class'NiceVeterancyTypes'.static.hasSkill(NicePlayerController(Instigator.Controller), class'NiceSkillSharpshooterHardWork')){
|
|
default.ZoomTime *= class'NiceSkillSharpshooterHardWork'.default.zoomSpeedBonus;
|
|
PlayerIronSightFOV *= class'NiceSkillSharpshooterHardWork'.default.zoomBonus;
|
|
}
|
|
super.ZoomOut(bAnimateTransition);
|
|
}
|
|
|
|
simulated function Destroyed(){
|
|
if(LaserDot != none)
|
|
LaserDot.Destroy();
|
|
if(altLaserDot != none)
|
|
altLaserDot.Destroy();
|
|
|
|
if(LaserAttachment != none)
|
|
LaserAttachment.Destroy();
|
|
if(altLaserAttachment != none)
|
|
altLaserAttachment.Destroy();
|
|
|
|
super(KFWeapon).Destroyed();
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
variantSwapTime=0.2
|
|
autoReloadSpeedModifier=1.0
|
|
secondaryCharge=1
|
|
recordedZoomTime=-1.000000
|
|
LaserAttachmentClass=Class'ScrnBalanceSrv.ScrnLaserAttachmentFirstPerson'
|
|
LaserDotClass=Class'ScrnBalanceSrv.ScrnLocalLaserDot'
|
|
LaserAttachmentBone="LightBone"
|
|
MagazineBone="Magazine"
|
|
bHasChargePhase=True
|
|
activeSlowdown=0.850000
|
|
activeSpeedup=1.150000
|
|
activeWindow=0.060000
|
|
bCanActiveReload=true
|
|
bModeZeroCanDryFire=True
|
|
bHasSecondaryAmmo=false
|
|
bUseDynamicLights=true
|
|
} |