//============================================================================= // KFWeap_Blunt_MedicBat //============================================================================= // A melee weapon that creates healing gas and can smack heals into teammates //============================================================================= // Killing Floor 2 // Copyright (C) 2018 Tripwire Interactive LLC //============================================================================= // Extends MeleeBase with code from KFWeap_HealerBase (for healing dart ammo functionality) class KFWeap_Blunt_MedicBat extends KFWeap_MeleeBase; /** Explosion actor class to spawn */ var class ExplosionActorClass; var() KFGameExplosion ExplosionTemplate; var() KFGameExplosion LightAttackExplosionTemplate; /** Whether Friendly Fire is enabled for the game */ var bool bFriendlyFireEnabled; /** Amount to heal when hitting a teammate per firemode */ var array AttackHealAmounts; /** Ammo dart cost for healing a teammate per firemode */ var array AttackHealCosts; /** How many points of heal ammo to recharge per second */ var(Healing) float HealFullRechargeSeconds; /** Keeps track of incremental healing since we have to heal in whole integers */ var float HealingIncrement; /** How many points of heal ammo to recharge per second. Calculated from the HealFullRechargeSeconds */ var float HealRechargePerSecond; /** Current amount of healing darts available */ var repnotify byte HealingDartAmmo; /** The actor the alt attack explosion should attach to */ var transient Actor BlastAttachee; /** The hit location of the blast */ var vector BlastHitLocation; /** Spawn location offset to improve cone hit detection */ var transient float BlastSpawnOffset; /** Starting Damage radius of the alt attack explosion*/ var float StartingDamageRadius; /** Animations that play in reaction to hitting with the alt fire attack*/ const HardFire_L = 'HardFire_L'; const HardFire_R = 'HardFire_R'; const HardFire_F = 'HardFire_F'; const HardFire_B = 'HardFire_B'; /** Damage type that is used when hitting a teammate with an attack */ var class HealingDamageType; /********************************************************************************************* @name Optics UI ********************************************************************************************* */ var class OpticsUIClass; var KFGFxWorld_MedicOptics OpticsUI; /** The last updated value for our ammo - Used to know when to update our optics ammo */ var byte StoredPrimaryAmmo; var byte StoredSecondaryAmmo; replication { if (bNetInitial) bFriendlyFireEnabled; if (bNetDirty && Role == ROLE_Authority && bAllowClientAmmoTracking) HealingDartAmmo; } /* epic =============================================== * ::ReplicatedEvent * * Called when a variable with the property flag "RepNotify" is replicated * * ===================================================== */ simulated event ReplicatedEvent(name VarName) { if (VarName == nameof(HealingDartAmmo)) { AmmoCount[ALTFIRE_FIREMODE] = HealingDartAmmo; } else { Super.ReplicatedEvent(VarName); } } /** * HealAmmo Regen client and server */ simulated event Tick(FLOAT DeltaTime) { // If we're not fully charged tick the HealAmmoRegen system if (AmmoCount[ALTFIRE_FIREMODE] < MagazineCapacity[ALTFIRE_FIREMODE]) { HealAmmoRegeneration(DeltaTime); } if (Instigator != none && Instigator.weapon == self) { UpdateOpticsUI(); } Super.Tick(DeltaTime); } simulated function ConsumeAmmoDarts(int AmmoDartCost) { // Handles the consumption of ammo darts for the default attack // If AmmoCount is being replicated, don't allow the client to modify it here if (Role == ROLE_Authority || bAllowClientAmmoTracking) { // Don't consume ammo if magazine size is 0 (infinite ammo with no reload) if (MagazineCapacity[1] > 0 && AmmoCount[1] > 0) { // Reduce ammo amount by heal ammo cost AmmoCount[1] = Max(AmmoCount[1] - AmmoDartCost, 0); } } } /** @see KFWeapon::ConsumeAmmo */ simulated function ConsumeAmmo(byte FireModeNum) { // If its not the healing fire mode (the heavy attack ammo consumption), use the super's consume if (FireModeNum != DEFAULT_FIREMODE) { Super.ConsumeAmmo(FireModeNum); return; } ConsumeAmmoDarts(AmmoCost[DEFAULT_FIREMODE]); } /** Overridden to call StartHealRecharge on server */ function GivenTo(Pawn thisPawn, optional bool bDoNotActivate) { super.GivenTo(thisPawn, bDoNotActivate); // StartHealRecharge gets called on the client from ClientWeaponSet (called from ClientGivenTo, called from GivenTo), // but we also need this called on the server for remote clients, since the server needs to track the ammo too (to know if/when to spawn projectiles) if (Role == ROLE_Authority && !thisPawn.IsLocallyControlled()) { StartHealRecharge(); } } /** Start the heal recharge cycle */ function StartHealRecharge() { local KFPerk InstigatorPerk; local float UsedHealRechargeTime; // begin ammo recharge on server if (Role == ROLE_Authority) { InstigatorPerk = GetPerk(); UsedHealRechargeTime = HealFullRechargeSeconds * static.GetUpgradeHealRechargeMod(CurrentWeaponUpgradeIndex); InstigatorPerk.ModifyHealerRechargeTime(UsedHealRechargeTime); // Set the healing recharge rate whenever we start charging HealRechargePerSecond = MagazineCapacity[ALTFIRE_FIREMODE] / UsedHealRechargeTime; HealingIncrement = 0; } } /** Heal Ammo Regen */ function HealAmmoRegeneration(float DeltaTime) { if (Role == ROLE_Authority) { HealingIncrement += HealRechargePerSecond * DeltaTime; if (HealingIncrement >= 1.0 && AmmoCount[ALTFIRE_FIREMODE] < MagazineCapacity[ALTFIRE_FIREMODE]) { AmmoCount[ALTFIRE_FIREMODE]++; HealingIncrement -= 1.0; // Heal ammo regen is only tracked on the server, so even though we told the client he could // keep track of ammo himself like a big boy, we still have to spoon-feed it to him. if (bAllowClientAmmoTracking) { HealingDartAmmo = AmmoCount[ALTFIRE_FIREMODE]; } } } } simulated event bool HasAmmo(byte FireModeNum, optional int Amount) { // Default fire mode either has ammo to trigger the heal or needs to return true to still allow a basic swing if (FireModeNum == DEFAULT_FIREMODE) { return true; } return super.HasAmmo(FireModeNum, Amount); } reliable client function ClientWeaponSet(bool bOptionalSet, optional bool bDoNotActivate) { local KFInventoryManager KFIM; super.ClientWeaponSet(bOptionalSet, bDoNotActivate); if (OpticsUI == none) { KFIM = KFInventoryManager(InvManager); if (KFIM != none) { //Create the screen's UI piece OpticsUI = KFGFxWorld_MedicOptics(KFIM.GetOpticsUIMovie(OpticsUIClass)); } } // Initialize our displayed ammo count and healer charge StartHealRecharge(); } function ItemRemovedFromInvManager() { local KFInventoryManager KFIM; local KFWeap_Blunt_MedicBat KFW; Super.ItemRemovedFromInvManager(); if (OpticsUI != none) { KFIM = KFInventoryManager(InvManager); if (KFIM != none) { foreach KFIM.InventoryActors(class'KFWeap_Blunt_MedicBat', KFW) { if (KFW != self && KFW.OpticsUI.Class == OpticsUI.class) { // A different weapon is still using this optics class return; } } //Create the screen's UI piece KFIM.RemoveOpticsUIMovie(OpticsUI.class); OpticsUI.Close(); OpticsUI = none; } } } /** Unpause our optics movie and reinitialize our ammo when we equip the weapon */ simulated function AttachWeaponTo(SkeletalMeshComponent MeshCpnt, optional Name SocketName) { super.AttachWeaponTo(MeshCpnt, SocketName); if (OpticsUI != none) { OpticsUI.SetPause(false); UpdateOpticsUI(true); OpticsUI.SetShotPercentCost(AmmoCost[ALTFIRE_FIREMODE]); } } /** Pause the optics movie once we unequip the weapon so it's not playing in the background */ simulated function DetachWeapon() { local Pawn OwnerPawn; super.DetachWeapon(); OwnerPawn = Pawn(Owner); if (OwnerPawn != none && OwnerPawn.Weapon == self) { if (OpticsUI != none) { OpticsUI.SetPause(); } } } /** * Update our displayed ammo count if it's changed */ simulated function UpdateOpticsUI(optional bool bForceUpdate) { if (OpticsUI != none && OpticsUI.OpticsContainer != none) { if (AmmoCount[DEFAULT_FIREMODE] != StoredPrimaryAmmo || bForceUpdate) { StoredPrimaryAmmo = AmmoCount[DEFAULT_FIREMODE]; OpticsUI.SetPrimaryAmmo(StoredPrimaryAmmo); } if (AmmoCount[ALTFIRE_FIREMODE] != StoredSecondaryAmmo || bForceUpdate) { StoredSecondaryAmmo = AmmoCount[ALTFIRE_FIREMODE]; OpticsUI.SetHealerCharge(StoredSecondaryAmmo); } if (OpticsUI.MinPercentPerShot != AmmoCost[ALTFIRE_FIREMODE]) { OpticsUI.SetShotPercentCost(1); } } } /** Healing charge doesn't count as ammo for purposes of inventory management (e.g. switching) */ simulated function bool HasAnyAmmo() { // Special ammo is stored in the default firemode (heal darts are separate) if (HasSpareAmmo() || AmmoCount[DEFAULT_FIREMODE] >= AmmoCost[CUSTOM_FIREMODE]) { return true; } return false; } /** Determines the secondary ammo left for HUD display */ simulated function int GetSecondaryAmmoForHUD() { return AmmoCount[1]; } simulated event PreBeginPlay() { Super.PreBeginPlay(); /** Initially check whether friendly fire is on or not. */ if (Role == ROLE_Authority && KFGameInfo(WorldInfo.Game).FriendlyFireScale != 0.f) { bFriendlyFireEnabled = true; } if (ExplosionTemplate != none) { StartingDamageRadius = ExplosionTemplate.DamageRadius; } } /** should be able to interrupt its reload state with any melee attack */ simulated function bool CanOverrideMagReload(byte FireModeNum) { return FireModeNum != RELOAD_FIREMODE; } /** Override to allow for two different states associated with RELOAD_FIREMODE */ simulated function SendToFiringState(byte FireModeNum) { // Ammo needs to be synchronized on client/server for this to work! if (FireModeNum == RELOAD_FIREMODE && !Super(KFWeapon).CanReload()) { SetCurrentFireMode(FireModeNum); GotoState('WeaponUpkeep'); return; } Super.SendToFiringState(FireModeNum); } /** Always allow reload and choose the correct state in SendToFiringState() */ simulated function bool CanReload(optional byte FireModeNum) { return true; } /** Skip calling StillFiring/PendingFire to fix log warning */ simulated function bool ShouldRefire() { if (CurrentFireMode == CUSTOM_FIREMODE) return false; return Super.ShouldRefire(); } simulated protected function PrepareExplosion() { local KFPlayerController KFPC; local KFPerk InstigatorPerk; ExplosionTemplate = default.ExplosionTemplate; ExplosionTemplate.DamageRadius = StartingDamageRadius; // Change the radius and damage based on the perk if (Owner.Role == ROLE_Authority) { KFPC = KFPlayerController(Instigator.Controller); if (KFPC != none) { `Log("RADIUS BEFORE: " $ExplosionTemplate.DamageRadius); InstigatorPerk = KFPC.GetPerk(); ExplosionTemplate.DamageRadius *= InstigatorPerk.GetAoERadiusModifier(); `Log("RADIUS BEFORE: " $ExplosionTemplate.DamageRadius); } } ExplosionActorClass = default.ExplosionActorClass; } /** Get the hard fire anim when the alt fire attack connects */ simulated function name GetWeaponFireAnim(byte FireModeNum) { // Adjust cone fire angle based on swing direction switch (MeleeAttackHelper.CurrentAttackDir) { case DIR_Forward: case DIR_ForwardLeft: case DIR_ForwardRight: return HardFire_F; case DIR_Backward: case DIR_BackwardLeft: case DIR_BackwardRight: return HardFire_B; case DIR_Left: return HardFire_L; case DIR_Right: return HardFire_R; } return ''; } simulated function SpawnExplosionFromTemplate(KFGameExplosion Template) { local KFExplosionActor ExploActor; local vector SpawnLoc; local rotator SpawnRot; SpawnLoc = BlastHitLocation; SpawnRot = GetAdjustedAim(SpawnLoc); // explode using the given template ExploActor = Spawn(ExplosionActorClass, self, , SpawnLoc, SpawnRot, , true); if (ExploActor != None) { ExploActor.InstigatorController = Instigator.Controller; ExploActor.Instigator = Instigator; ExplosionTemplate.bFullDamageToAttachee = true; KFExplosionActorReplicated(ExploActor).bIgnoreInstigator = false; ExploActor.bReplicateInstigator = true; ExploActor.Explode(Template, vector(SpawnRot)); } // Reset damage radius ExplosionTemplate.DamageRadius = StartingDamageRadius; } simulated function CustomFire() { if (Instigator.Role < ROLE_Authority) { return; } PrepareExplosion(); SpawnExplosionFromTemplate(ExplosionTemplate); // tell remote clients that we fired, to trigger effects in third person IncrementFlashCount(); } simulated function HealTeammateWithAttack(Actor HitActor, vector HitLocation, float HealingAmount, byte HealCost) { local KFPawn Victim; if (Role == ROLE_Authority) { if (Instigator != None) { // only detonate when it hits a pawn so that level geometry doesn't get in the way if (HitActor.bWorldGeometry) { return; } Victim = KFPawn(HitActor); // If the victim is a teammate and the player has default ammo (healing darts) then heal this teammate // also make sure the victim is still alive and is actually missing health if (Victim != None && (Victim.GetTeamNum() == Instigator.GetTeamNum()) && Victim.Health > 0 && Victim.Health < Victim.HealthMax) { if (AmmoCount[1] >= HealCost) { ConsumeAmmoDarts(HealCost); Victim.HealDamage(HealingAmount, Instigator.Controller, HealingDamageType); if (Instigator.Role >= ROLE_Authority) { BlastHitLocation = HitLocation; SpawnExplosionFromTemplate(LightAttackExplosionTemplate); } } } } } } // BASH simulated state MeleeAttackBasic { /** Network: Local Player */ simulated function NotifyMeleeCollision(Actor HitActor, optional vector HitLocation) { HealTeammateWithAttack(HitActor, HitLocation, AttackHealAmounts[BASH_FIREMODE], AttackHealCosts[BASH_FIREMODE]); } } // LIGHT ATTACK simulated state MeleeChainAttacking { /** Network: Local Player */ simulated function NotifyMeleeCollision(Actor HitActor, optional vector HitLocation) { HealTeammateWithAttack(HitActor, HitLocation, AttackHealAmounts[DEFAULT_FIREMODE], AttackHealCosts[DEFAULT_FIREMODE]); } } // HEAVY ATTACK simulated state MeleeHeavyAttacking { /** Network: Local Player */ simulated function NotifyMeleeCollision(Actor HitActor, optional vector HitLocation) { local KFPawn Victim; if (Instigator != None) { // only detonate when it hits a pawn so that level geometry doesn't get in the way if (HitActor.bWorldGeometry) { return; } Victim = KFPawn(HitActor); if (Victim == None || (Victim.bPlayedDeath && `TimeSince(Victim.TimeOfDeath) > 0.f) ) { return; } if (AmmoCount[0] >= AmmoCost[CUSTOM_FIREMODE] && !IsTimerActive(nameof(BeginMedicBatExplosion))) { BlastAttachee = HitActor; BlastHitLocation = HitLocation; // need to delay one frame, since this is called from AnimNotify SetTimer(0.001f, false, nameof(BeginMedicBatExplosion)); if (Role < ROLE_Authority && Instigator.IsLocallyControlled()) { if (!HitActor.bTearOff || Victim == none) { ServerBeginMedicBatExplosion(HitActor, HitLocation); } } } else { HealTeammateWithAttack(HitActor, HitLocation, AttackHealAmounts[HEAVY_ATK_FIREMODE], AttackHealCosts[HEAVY_ATK_FIREMODE]); } } } } /** Called on the server */ reliable server private function ServerBeginMedicBatExplosion(Actor HitActor, optional vector HitLocation) { // Ignore if too far away (something went wrong!) if (VSizeSq2D(HitLocation - Instigator.Location) > Square(500)) { return; } BlastHitLocation = HitLocation; BlastAttachee = HitActor; SendToFiringState(CUSTOM_FIREMODE); } /** Called when altfire melee attack hits a target and there is ammo left */ simulated function BeginMedicBatExplosion() { SendToFiringState(CUSTOM_FIREMODE); } /********************************************************************************************* * State Active * A Weapon this is being held by a pawn should be in the active state. In this state, * a weapon should loop any number of idle animations, as well as check the PendingFire flags * to see if a shot has been fired. *********************************************************************************************/ simulated state Active { /** * Called from Weapon:Active.BeginState when HasAnyAmmo (which is overridden above) returns false. */ simulated function WeaponEmpty() { local int i; // Copied from Weapon:Active.BeginState where HasAnyAmmo returns true. // Basically, pretend the weapon isn't empty in this case. for (i=0; i