player_list = {}; class_viewheight_map = { [TF_CLASS_SCOUT] = 65, [TF_CLASS_SOLDIER] = 68, [TF_CLASS_PYRO] = 68, [TF_CLASS_DEMOMAN] = 68, [TF_CLASS_HEAVYWEAPONS] = 75, [TF_CLASS_ENGINEER] = 68, [TF_CLASS_MEDIC] = 75, [TF_CLASS_SNIPER] = 75, [TF_CLASS_SPY] = 75, }; -- Grab the CEntity table local _ = Entity("info_target", false, false); local CEntity = getmetatable(_); _:Remove(); -- Get player eye angles CEntity.GetEyeAngles = function(self) if (self and self:IsValid() and self:IsPlayer()) then return Vector(self["m_angEyeAngles[0]"], self["m_angEyeAngles[1]"], 0); end end -- Get player eye position CEntity.GetEyePos = function(self) if (not self or not self:IsValid() or not self:IsPlayer()) then return; end local eyepos = self:GetAbsOrigin(); eyepos.z = eyepos.z + class_viewheight_map[self.m_iClass]; return eyepos end -- Check whether the passed player is in our FOV (not necessarily whether we can see them) CEntity.IsPlayerInFOV = function(self, player) if (not self or not self:IsValid() or not self:IsPlayer()) then return; end if (not player or not player:IsValid() or not player:IsPlayer()) then return; end local tolerance = 0.5736; -- cos(110/2) local eyepos = self:GetEyePos(); local eyefwd = self:GetEyeAngles():GetForward(); local playerorigin = player:GetAbsOrigin() -- We go eyepos -> origin because you're more likely to see the top of a player -- rather than their feet due to map geometry -- What can we see from [eyepos, origin) ? -- Checks target eyepos and center for i=1,2 do playerorigin.z = playerorigin.z + class_viewheight_map[player.m_iClass] / i; delta = (playerorigin - eyepos):Normalize(); if (eyefwd:Dot(delta) >= tolerance) then return true; end end -- Can we see target's origin? local delta = (player:GetAbsOrigin() - eyepos):Normalize(); if (eyefwd:Dot(delta) >= tolerance) then return true; end -- Not in our FOV return false; end function PlayerParry(player, wep, viewmodel, idleseq, inspectseq, playbackrate, idletime, attacktime, parrytimemin, parrytimemax) local playerdata = player_list[player:GetUserId()]; local curtime = CurTime(); -- We only want to parry when idle -- We also check next attack time for if we idle early if (viewmodel.m_nSequence == idleseq and curtime >= wep.m_flNextPrimaryAttack) then player:AddCond(TF_COND_CANNOT_SWITCH_FROM_MELEE, 1); player:PlaySoundToSelf("weapons/demo_sword_swing"..math.random(1,3)..".wav"); wep.m_flTimeWeaponIdle = curtime + idletime; wep.m_flNextPrimaryAttack = curtime + attacktime; viewmodel.m_flPlaybackRate = playbackrate; viewmodel.m_flCycle = 0.0; viewmodel.m_nSequence = inspectseq; timer.Create(parrytimemin, function() if (not playerdata) then return; end playerdata.demo_parry_count = 0; playerdata.demo_parrying = true; end, 1); timer.Create(parrytimemax, function() if (not playerdata) then return; end playerdata.demo_parry_count = 0; playerdata.demo_parrying = false; end, 1); end end -- Called on player key press function OnPlayerKey(player, key) if (not player or not player:IsValid() or not player:IsAlive()) then return end; local userid = player:GetUserId(); local playerdata = player_list[userid]; local wep = player.m_hActiveWeapon; -- Mouse2 if (key == IN_ATTACK2) then if (player:GetPlayerItemBySlot(LOADOUT_POSITION_MELEE) == wep) then if (player.m_iClass == TF_CLASS_DEMOMAN) then if (not wep or not wep:IsValid()) then return; end local viewmodel = player.m_hViewModel[1] or player.m_hViewModel[2]; -- Persian Persuader if (wep.m_iItemDefinitionIndex == 404) then PlayerParry(player, wep, viewmodel, 8, 48, 1.5, 0.7, 1, 0.5, 1.5); -- Eyelander, Skullcutter, Claidheamohmor, Katana elseif (wep.m_iClassname == "tf_weapon_sword" or wep.m_iClassname == "tf_weapon_katana") then PlayerParry(player, wep, viewmodel, 13, 53, 5, 1, 1, 0.5, 1.5); end end end end end -- Called before player takes damage function OnPlayerDamagedPre(player, damageinfo) local damagecustom = damageinfo.DamageCustom; local attacker = damageinfo.Attacker; local playerdata = player_list[player:GetUserId()]; -- Should we parry our attacker? if (playerdata.demo_parrying and attacker and attacker:IsValid() and attacker:IsPlayer() and attacker.m_iTeamNum ~= player.m_iTeamNum and damageinfo.DamageType & DMG_CLUB == DMG_CLUB) then -- We're parrying a valid melee attack, can we see our attacker and are we able to parry? if (playerdata.demo_parry_count < playerdata.demo_max_parry_count and player:IsPlayerInFOV(attacker)) then player:AddCond(TF_COND_SPEED_BOOST, 4); if (attacker.m_bIsMiniBoss == 1) then attacker:StunPlayer(2, 0.4, TF_STUNFLAG_SLOWDOWN | TF_STUNFLAG_NOSOUNDOREFFECT, player); damageinfo.Damage = damageinfo.Damage * 0.5; else attacker:StunPlayer(1.5, 1, TF_STUNFLAGS_NORMALBONK, player); damageinfo.Damage = 0; end player:PlaySoundToSelf("weapons/samurai/tf_katana_impact_object_0"..math.random(1,3)..".wav"); playerdata.demo_parry_count = playerdata.demo_parry_count + 1; return true; end end end -- Called on player connected to server function OnPlayerConnected(player) local userid = player:GetUserId(); if (player:IsRealPlayer()) then player_list[userid] = { demo_parrying = false, -- Are we parrying attacks at the moment? demo_parry_count = 0, -- How many melee attacks have we blocked this parry? demo_max_parry_count = 5, -- How many melee attacks are we allowed to block per parry? }; -- Callbacks player:AddCallback(ON_KEY_PRESSED, OnPlayerKey); player:AddCallback(ON_DAMAGE_RECEIVED_PRE, OnPlayerDamagedPre); end end -- Called on player disconnected from server function OnPlayerDisconnected(player) local userid = player:GetUserId() local playerdata = player_list[userid]; if (player:IsRealPlayer() and playerdata) then player_list[userid] = nil; end end