sourcemod-plugins/scripting/l4d2_skill_detect.sp

3125 lines
No EOL
123 KiB
SourcePawn

/**
* L4D2_skill_detect
*
* Plugin to detect and forward reports about 'skill'-actions,
* such as skeets, crowns, levels, dp's.
* Works in campaign and versus modes.
*
* m_isAttemptingToPounce can only be trusted for
* AI hunters -- for human hunters this gets cleared
* instantly on taking killing damage
*
* Shotgun skeets and teamskeets are only counted if the
* added up damage to pounce_interrupt is done by shotguns
* only. 'Skeeting' chipped hunters shouldn't count, IMO.
*
* This performs global forward calls to:
* OnSkeet( survivor, hunter )
* OnSkeetMelee( survivor, hunter )
* OnSkeetGL( survivor, hunter )
* OnSkeetSniper( survivor, hunter )
* OnSkeetHurt( survivor, hunter, damage, isOverkill )
* OnSkeetMeleeHurt( survivor, hunter, damage, isOverkill )
* OnSkeetSniperHurt( survivor, hunter, damage, isOverkill )
* OnHunterDeadstop( survivor, hunter )
* OnBoomerPop( survivor, boomer, shoveCount, Float:timeAlive )
* OnChargerLevel( survivor, charger )
* OnChargerLevelHurt( survivor, charger, damage )
* OnWitchCrown( survivor, damage )
* OnWitchCrownHurt( survivor, damage, chipdamage )
* OnTongueCut( survivor, smoker )
* OnSmokerSelfClear( survivor, smoker, withShove )
* OnTankRockSkeeted( survivor, tank )
* OnTankRockEaten( tank, survivor )
* OnHunterHighPounce( hunter, victim, actualDamage, Float:calculatedDamage, Float:height, bool:bReportedHigh, bool:bPlayerIncapped )
* OnJockeyHighPounce( jockey, victim, Float:height, bool:bReportedHigh )
* OnDeathCharge( charger, victim, Float: height, Float: distance, wasCarried )
* OnSpecialShoved( survivor, infected, zombieClass )
* OnSpecialClear( clearer, pinner, pinvictim, zombieClass, Float:timeA, Float:timeB, withShove )
* OnBoomerVomitLanded( boomer, amount )
* OnBunnyHopStreak( survivor, streak, Float:maxVelocity )
* OnCarAlarmTriggered( survivor, infected, reason )
*
* OnDeathChargeAssist( assister, charger, victim ) [ not done yet ]
* OnBHop( player, isInfected, speed, streak ) [ not done yet ]
*
* Where survivor == -2 if it was a team effort, -1 or 0 if unknown or invalid client.
* damage is the amount of damage done (that didn't add up to skeeting damage),
* and isOverkill indicates whether the shot would've been a skeet if the hunter
* had not been chipped.
*
* @author Tabun
* @libraryname skill_detect
*/
#pragma semicolon 1
#include <sourcemod>
#include <sdkhooks>
#include <sdktools>
#include <left4dhooks>
#define PLUGIN_VERSION "1.0"
#define IS_VALID_CLIENT(%1) (%1 > 0 && %1 <= MaxClients)
#define IS_SURVIVOR(%1) (GetClientTeam(%1) == 2)
#define IS_INFECTED(%1) (GetClientTeam(%1) == 3)
#define IS_VALID_INGAME(%1) (IS_VALID_CLIENT(%1) && IsClientInGame(%1))
#define IS_VALID_SURVIVOR(%1) (IS_VALID_INGAME(%1) && IS_SURVIVOR(%1))
#define IS_VALID_INFECTED(%1) (IS_VALID_INGAME(%1) && IS_INFECTED(%1))
#define IS_SURVIVOR_ALIVE(%1) (IS_VALID_SURVIVOR(%1) && IsPlayerAlive(%1))
#define IS_INFECTED_ALIVE(%1) (IS_VALID_INFECTED(%1) && IsPlayerAlive(%1))
#define QUOTES(%1) (%1)
#define SHOTGUN_BLAST_TIME 0.1
#define POUNCE_CHECK_TIME 0.1
#define HOP_CHECK_TIME 0.1
#define HOPEND_CHECK_TIME 0.1 // after streak end (potentially) detected, to check for realz?
#define SHOVE_TIME 0.05
#define MAX_CHARGE_TIME 12.0 // maximum time to pass before charge checking ends
#define CHARGE_CHECK_TIME 0.25 // check interval for survivors flying from impacts
#define CHARGE_END_CHECK 2.5 // after client hits ground after getting impact-charged: when to check whether it was a death
#define CHARGE_END_RECHECK 3.0 // safeguard wait to recheck on someone getting incapped out of bounds
#define VOMIT_DURATION_TIME 2.25 // how long the boomer vomit stream lasts -- when to check for boom count
#define ROCK_CHECK_TIME 0.34 // how long to wait after rock entity is destroyed before checking for skeet/eat (high to avoid lag issues)
#define CARALARM_MIN_TIME 0.11 // maximum time after touch/shot => alarm to connect the two events (test this for LAG)
#define WITCH_CHECK_TIME 0.1 // time to wait before checking for witch crown after shoots fired
#define WITCH_DELETE_TIME 0.15 // time to wait before deleting entry from witch trie after entity is destroyed
#define MIN_DC_TRIGGER_DMG 300 // minimum amount a 'trigger' / drown must do before counted as a death action
#define MIN_DC_FALL_DMG 175 // minimum amount of fall damage counts as death-falling for a deathcharge
#define WEIRD_FLOW_THRESH 900.0 // -9999 seems to be break flow.. but meh
#define MIN_FLOWDROPHEIGHT 350.0 // minimum height a survivor has to have dropped before a WEIRD_FLOW value is treated as a DC spot
#define MIN_DC_RECHECK_DMG 100 // minimum damage from map to have taken on first check, to warrant recheck
#define HOP_ACCEL_THRESH 0.01 // bhop speed increase must be higher than this for it to count as part of a hop streak
#define ZC_SMOKER 1
#define ZC_BOOMER 2
#define ZC_HUNTER 3
#define ZC_JOCKEY 5
#define ZC_CHARGER 6
#define ZC_TANK 8
#define HITGROUP_HEAD 1
#define DMG_CRUSH (1 << 0) // crushed by falling or moving object.
#define DMG_BULLET (1 << 1) // shot
#define DMG_SLASH (1 << 2) // cut, clawed, stabbed
#define DMG_CLUB (1 << 7) // crowbar, punch, headbutt
#define DMG_BUCKSHOT (1 << 29) // not quite a bullet. Little, rounder, different.
#define DMGARRAYEXT 7 // MAXPLAYERS+# -- extra indices in witch_dmg_array + 1
#define CUT_SHOVED 1 // smoker got shoved
#define CUT_SHOVEDSURV 2 // survivor got shoved
#define CUT_KILL 3 // reason for tongue break (release_type)
#define CUT_SLASH 4 // this is used for others shoving a survivor free too, don't trust .. it involves tongue damage?
#define VICFLG_CARRIED (1 << 0) // was the one that the charger carried (not impacted)
#define VICFLG_FALL (1 << 1) // flags stored per charge victim, to check for deathchargeroony -- fallen
#define VICFLG_DROWN (1 << 2) // drowned
#define VICFLG_HURTLOTS (1 << 3) // whether the victim was hurt by 400 dmg+ at once
#define VICFLG_TRIGGER (1 << 4) // killed by trigger_hurt
#define VICFLG_AIRDEATH (1 << 5) // died before they hit the ground (impact check)
#define VICFLG_KILLEDBYOTHER (1 << 6) // if the survivor was killed by an SI other than the charger
#define VICFLG_WEIRDFLOW (1 << 7) // when survivors get out of the map and such
#define VICFLG_WEIRDFLOWDONE (1 << 8) // checked, don't recheck for this
#define REP_SKEET (1 << 0)
#define REP_HURTSKEET (1 << 1)
#define REP_LEVEL (1 << 2)
#define REP_HURTLEVEL (1 << 3)
#define REP_CROWN (1 << 4)
#define REP_DRAWCROWN (1 << 5)
#define REP_TONGUECUT (1 << 6)
#define REP_SELFCLEAR (1 << 7)
#define REP_SELFCLEARSHOVE (1 << 8)
#define REP_ROCKSKEET (1 << 9)
#define REP_DEADSTOP (1 << 10)
#define REP_POP (1 << 11)
#define REP_SHOVE (1 << 12)
#define REP_HUNTERDP (1 << 13)
#define REP_JOCKEYDP (1 << 14)
#define REP_DEATHCHARGE (1 << 15)
#define REP_DC_ASSIST (1 << 16)
#define REP_INSTACLEAR (1 << 17) // 131072
#define REP_BHOPSTREAK (1 << 18) // 262144
#define REP_CARALARM (1 << 19) // 524288
#define REP_DEFAULT "581685" // (REP_SKEET | REP_LEVEL | REP_CROWN | REP_DRAWCROWN | REP_HUNTERDP | REP_JOCKEYDP | REP_DEATHCHARGE | REP_CARALARM)
// 1 4 16 32 8192 16384 32768 65536 (122933 with ASSIST, 57397 without); 131072 for instaclears + 524288 for car alarm
// trie values: weapon type
enum strWeaponType
{
WPTYPE_SNIPER,
WPTYPE_MAGNUM,
WPTYPE_GL
};
// trie values: OnEntityCreated classname
enum strOEC
{
OEC_WITCH,
OEC_TANKROCK,
OEC_TRIGGER,
OEC_CARALARM,
OEC_CARGLASS
};
// trie values: special abilities
enum strAbility
{
ABL_HUNTERLUNGE,
ABL_ROCKTHROW
};
enum
{
rckDamage,
rckTank,
rckSkeeter,
strRockData
};
// witch array entries (maxplayers+index)
enum
{
WTCH_NONE,
WTCH_HEALTH,
WTCH_GOTSLASH,
WTCH_STARTLED,
WTCH_CROWNER,
WTCH_CROWNSHOT,
WTCH_CROWNTYPE,
strWitchArray
};
enum
{
CALARM_UNKNOWN,
CALARM_HIT,
CALARM_TOUCHED,
CALARM_EXPLOSION,
CALARM_BOOMER,
enAlarmReasons
};
new const String: g_csSIClassName[][] =
{
"",
"smoker",
"boomer",
"hunter",
"spitter",
"jockey",
"charger",
"witch",
"tank"
};
new bool: g_bLateLoad = false;
new Handle: g_hForwardSkeet = INVALID_HANDLE;
new Handle: g_hForwardSkeetHurt = INVALID_HANDLE;
new Handle: g_hForwardSkeetMelee = INVALID_HANDLE;
new Handle: g_hForwardSkeetMeleeHurt = INVALID_HANDLE;
new Handle: g_hForwardSkeetSniper = INVALID_HANDLE;
new Handle: g_hForwardSkeetSniperHurt = INVALID_HANDLE;
new Handle: g_hForwardSkeetGL = INVALID_HANDLE;
new Handle: g_hForwardHunterDeadstop = INVALID_HANDLE;
new Handle: g_hForwardSIShove = INVALID_HANDLE;
new Handle: g_hForwardBoomerPop = INVALID_HANDLE;
new Handle: g_hForwardLevel = INVALID_HANDLE;
new Handle: g_hForwardLevelHurt = INVALID_HANDLE;
new Handle: g_hForwardCrown = INVALID_HANDLE;
new Handle: g_hForwardDrawCrown = INVALID_HANDLE;
new Handle: g_hForwardTongueCut = INVALID_HANDLE;
new Handle: g_hForwardSmokerSelfClear = INVALID_HANDLE;
new Handle: g_hForwardRockSkeeted = INVALID_HANDLE;
new Handle: g_hForwardRockEaten = INVALID_HANDLE;
new Handle: g_hForwardHunterDP = INVALID_HANDLE;
new Handle: g_hForwardJockeyDP = INVALID_HANDLE;
new Handle: g_hForwardDeathCharge = INVALID_HANDLE;
new Handle: g_hForwardClear = INVALID_HANDLE;
new Handle: g_hForwardVomitLanded = INVALID_HANDLE;
new Handle: g_hForwardBHopStreak = INVALID_HANDLE;
new Handle: g_hForwardAlarmTriggered = INVALID_HANDLE;
new Handle: g_hTrieWeapons = INVALID_HANDLE; // weapon check
new Handle: g_hTrieEntityCreated = INVALID_HANDLE; // getting classname of entity created
new Handle: g_hTrieAbility = INVALID_HANDLE; // ability check
new Handle: g_hWitchTrie = INVALID_HANDLE; // witch tracking (Crox)
new Handle: g_hRockTrie = INVALID_HANDLE; // tank rock tracking
new Handle: g_hCarTrie = INVALID_HANDLE; // car alarm tracking
// all SI / pinners
new Float: g_fSpawnTime [MAXPLAYERS + 1]; // time the SI spawned up
new Float: g_fPinTime [MAXPLAYERS + 1][2]; // time the SI pinned a target: 0 = start of pin (tongue pull, charger carry); 1 = carry end / tongue reigned in
new g_iSpecialVictim [MAXPLAYERS + 1]; // current victim (set in traceattack, so we can check on death)
// hunters: skeets/pounces
new g_iHunterShotDmgTeam [MAXPLAYERS + 1]; // counting shotgun blast damage for hunter, counting entire survivor team's damage
new g_iHunterShotDmg [MAXPLAYERS + 1][MAXPLAYERS + 1]; // counting shotgun blast damage for hunter / skeeter combo
new Float: g_fHunterShotStart [MAXPLAYERS + 1][MAXPLAYERS + 1]; // when the last shotgun blast on hunter started (if at any time) by an attacker
new Float: g_fHunterTracePouncing [MAXPLAYERS + 1]; // time when the hunter was still pouncing (in traceattack) -- used to detect pouncing status
new Float: g_fHunterLastShot [MAXPLAYERS + 1]; // when the last shotgun damage was done (by anyone) on a hunter
new g_iHunterLastHealth [MAXPLAYERS + 1]; // last time hunter took any damage, how much health did it have left?
new g_iHunterOverkill [MAXPLAYERS + 1]; // how much more damage a hunter would've taken if it wasn't already dead
new bool: g_bHunterKilledPouncing [MAXPLAYERS + 1]; // whether the hunter was killed when actually pouncing
new g_iPounceDamage [MAXPLAYERS + 1]; // how much damage on last 'highpounce' done
new Float: g_fPouncePosition [MAXPLAYERS + 1][3]; // position that a hunter (jockey?) pounced from (or charger started his carry)
// deadstops
new Float: g_fVictimLastShove [MAXPLAYERS + 1][MAXPLAYERS + 1]; // when was the player shoved last by attacker? (to prevent doubles)
// levels / charges
new g_iChargerHealth [MAXPLAYERS + 1]; // how much health the charger had the last time it was seen taking damage
new Float: g_fChargeTime [MAXPLAYERS + 1]; // time the charger's charge last started, or if victim, when impact started
new g_iChargeVictim [MAXPLAYERS + 1]; // who got charged
new Float: g_fChargeVictimPos [MAXPLAYERS + 1][3]; // location of each survivor when it got hit by the charger
new g_iVictimCharger [MAXPLAYERS + 1]; // for a victim, by whom they got charge(impacted)
new g_iVictimFlags [MAXPLAYERS + 1]; // flags stored per charge victim: VICFLAGS_
new g_iVictimMapDmg [MAXPLAYERS + 1]; // for a victim, how much the cumulative map damage is so far (trigger hurt / drowning)
// pops
new bool: g_bBoomerHitSomebody [MAXPLAYERS + 1]; // false if boomer didn't puke/exploded on anybody
new g_iBoomerGotShoved [MAXPLAYERS + 1]; // count boomer was shoved at any point
new g_iBoomerVomitHits [MAXPLAYERS + 1]; // how many booms in one vomit so far
// crowns
new Float: g_fWitchShotStart [MAXPLAYERS + 1]; // when the last shotgun blast from a survivor started (on any witch)
// smoker clears
new bool: g_bSmokerClearCheck [MAXPLAYERS + 1]; // [smoker] smoker dies and this is set, it's a self-clear if g_iSmokerVictim is the killer
new g_iSmokerVictim [MAXPLAYERS + 1]; // [smoker] the one that's being pulled
new g_iSmokerVictimDamage [MAXPLAYERS + 1]; // [smoker] amount of damage done to a smoker by the one he pulled
new bool: g_bSmokerShoved [MAXPLAYERS + 1]; // [smoker] set if the victim of a pull manages to shove the smoker
// rocks
new g_iTankRock [MAXPLAYERS + 1]; // rock entity per tank
new g_iRocksBeingThrown [10]; // 10 tanks max simultanously throwing rocks should be ok (this stores the tank client)
new g_iRocksBeingThrownCount = 0; // so we can do a push/pop type check for who is throwing a created rock
// hops
new bool: g_bIsHopping [MAXPLAYERS + 1]; // currently in a hop streak
new bool: g_bHopCheck [MAXPLAYERS + 1]; // flag to check whether a hopstreak has ended (if on ground for too long.. ends)
new g_iHops [MAXPLAYERS + 1]; // amount of hops in streak
new Float: g_fLastHop [MAXPLAYERS + 1][3]; // velocity vector of last jump
new Float: g_fHopTopVelocity [MAXPLAYERS + 1]; // maximum velocity in hopping streak
// alarms
new Float: g_fLastCarAlarm = 0.0; // time when last car alarm went off
new g_iLastCarAlarmReason [MAXPLAYERS + 1]; // what this survivor did to set the last alarm off
new g_iLastCarAlarmBoomer; // if a boomer triggered an alarm, remember it
// cvars
new Handle: g_hCvarReport = INVALID_HANDLE; // cvar whether to report at all
new Handle: g_hCvarReportFlags = INVALID_HANDLE; // cvar what to report
new Handle: g_hCvarAllowMelee = INVALID_HANDLE; // cvar whether to count melee skeets
new Handle: g_hCvarAllowSniper = INVALID_HANDLE; // cvar whether to count sniper headshot skeets
new Handle: g_hCvarAllowGLSkeet = INVALID_HANDLE; // cvar whether to count direct hit GL skeets
new Handle: g_hCvarDrawCrownThresh = INVALID_HANDLE; // cvar damage in final shot for drawcrown-req.
new Handle: g_hCvarSelfClearThresh = INVALID_HANDLE; // cvar damage while self-clearing from smokers
new Handle: g_hCvarHunterDPThresh = INVALID_HANDLE; // cvar damage for hunter highpounce
new Handle: g_hCvarJockeyDPThresh = INVALID_HANDLE; // cvar distance for jockey highpounce
new Handle: g_hCvarHideFakeDamage = INVALID_HANDLE; // cvar damage while self-clearing from smokers
new Handle: g_hCvarDeathChargeHeight = INVALID_HANDLE; // cvar how high a charger must have come in order for a DC to count
new Handle: g_hCvarInstaTime = INVALID_HANDLE; // cvar clear within this time or lower for instaclear
new Handle: g_hCvarBHopMinStreak = INVALID_HANDLE; // cvar this many hops in a row+ = streak
new Handle: g_hCvarBHopMinInitSpeed = INVALID_HANDLE; // cvar lower than this and the first jump won't be seen as the start of a streak
new Handle: g_hCvarBHopContSpeed = INVALID_HANDLE; // cvar
new Handle: g_hCvarPounceInterrupt = INVALID_HANDLE; // z_pounce_damage_interrupt
new g_iPounceInterrupt = 150;
new Handle: g_hCvarChargerHealth = INVALID_HANDLE; // z_charger_health
new Handle: g_hCvarWitchHealth = INVALID_HANDLE; // z_witch_health
new Handle: g_hCvarMaxPounceDistance = INVALID_HANDLE; // z_pounce_damage_range_max
new Handle: g_hCvarMinPounceDistance = INVALID_HANDLE; // z_pounce_damage_range_min
new Handle: g_hCvarMaxPounceDamage = INVALID_HANDLE; // z_hunter_max_pounce_bonus_damage;
/*
Reports:
--------
Damage shown is damage done in the last shot/slash. So for crowns, this means
that the 'damage' value is one shotgun blast
Quirks:
-------
Does not report people cutting smoker tongues that target players other
than themselves. Could be done, but would require (too much) tracking.
Actual damage done, on Hunter DPs, is low when the survivor gets incapped
by (a fraction of) the total pounce damage.
Fake Damage
-----------
Hiding of fake damage has the following consequences:
- Drawcrowns are less likely to be registered: if a witch takes too
much chip before the crowning shot, the final shot will be considered
as doing too little damage for a crown (even if it would have been a crown
had the witch had more health).
- Charger levels are harder to get on chipped chargers. Any charger that
has taken (600 - 390 =) 210 damage or more cannot be leveled (even if
the melee swing would've killed the charger (1559 damage) if it'd have
had full health).
I strongly recommend leaving fakedamage visible: it will offer more feedback on
the survivor's action and reward survivors doing (what would be) full crowns and
levels on chipped targets.
To Do
-----
- fix: tank rock owner is not reliable for the RockEaten forward
- fix: tank rock skeets still unreliable detection (often triggers a 'skeet' when actually landed on someone)
- fix: apparently some HR4 cars generate car alarm messages when shot, even when no alarm goes off
(combination with car equalize plugin?)
- see below: the single hook might also fix this.. -- if not, hook for sound
- do a hookoutput on prop_car_alarm's and use that to track the actual alarm
going off (might help in the case 2 alarms go off exactly at the same time?)
- fix: double prints on car alarms (sometimes? epi + m60)
- fix: sometimes instaclear reports double for single clear (0.16s / 0.19s) epi saw this, was for hunter
- fix: deadstops and m2s don't always register .. no idea why..
- fix: sometimes a (first?) round doesn't work for skeet detection.. no hurt/full skeets are reported or counted
- make forwards fire for every potential action,
- include the relevant values, so other plugins can decide for themselves what to consider it
- test chargers getting dislodged with boomer pops?
- add commonhop check
- add deathcharge assist check
- smoker
- jockey
- add deathcharge coordinates for some areas
- DT4 next to saferoom
- DA1 near the lower roof, on sidewalk next to fence (no hurttrigger there)
- DA2 next to crane roof to the right of window
DA2 charge down into start area, after everyone's jumped the fence
- count rock hits even if they do no damage [epi request]
- sir
- make separate teamskeet forward, with (for now, up to) 4 skeeters + the damage each did
- xan
- add detection/display of unsuccesful witch crowns (witch death + info)
detect...
- ? add jockey deadstops (and change forward to reflect type)
- ? speedcrown detection?
- ? spit-on-cap detection
---
done:
- applied sanity bounds to calculated damage for hunter dps
- removed tank's name from rock skeet print
- 300+ speed hops are considered hops even if no increase
*/
public Plugin:myinfo =
{
name = "Skill Detection (skeets, crowns, levels)",
author = "Tabun",
description = "Detects and reports skeets, crowns, levels, highpounces, etc.",
version = PLUGIN_VERSION,
url = "https://github.com/Tabbernaut/L4D2-Plugins"
}
public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
RegPluginLibrary("skill_detect");
g_hForwardSkeet = CreateGlobalForward("OnSkeet", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardSkeetHurt = CreateGlobalForward("OnSkeetHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell );
g_hForwardSkeetMelee = CreateGlobalForward("OnSkeetMelee", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardSkeetMeleeHurt = CreateGlobalForward("OnSkeetMeleeHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell );
g_hForwardSkeetSniper = CreateGlobalForward("OnSkeetSniper", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardSkeetSniperHurt = CreateGlobalForward("OnSkeetSniperHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell );
g_hForwardSkeetGL = CreateGlobalForward("OnSkeetGL", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardSIShove = CreateGlobalForward("OnSpecialShoved", ET_Ignore, Param_Cell, Param_Cell, Param_Cell );
g_hForwardHunterDeadstop = CreateGlobalForward("OnHunterDeadstop", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardBoomerPop = CreateGlobalForward("OnBoomerPop", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float );
g_hForwardLevel = CreateGlobalForward("OnChargerLevel", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardLevelHurt = CreateGlobalForward("OnChargerLevelHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell );
g_hForwardCrown = CreateGlobalForward("OnWitchCrown", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardDrawCrown = CreateGlobalForward("OnWitchDrawCrown", ET_Ignore, Param_Cell, Param_Cell, Param_Cell );
g_hForwardTongueCut = CreateGlobalForward("OnTongueCut", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardSmokerSelfClear = CreateGlobalForward("OnSmokerSelfClear", ET_Ignore, Param_Cell, Param_Cell, Param_Cell );
g_hForwardRockSkeeted = CreateGlobalForward("OnTankRockSkeeted", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardRockEaten = CreateGlobalForward("OnTankRockEaten", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardHunterDP = CreateGlobalForward("OnHunterHighPounce", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell, Param_Cell );
g_hForwardJockeyDP = CreateGlobalForward("OnJockeyHighPounce", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Cell );
g_hForwardDeathCharge = CreateGlobalForward("OnDeathCharge", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell );
g_hForwardClear = CreateGlobalForward("OnSpecialClear", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell );
g_hForwardVomitLanded = CreateGlobalForward("OnBoomerVomitLanded", ET_Ignore, Param_Cell, Param_Cell );
g_hForwardBHopStreak = CreateGlobalForward("OnBunnyHopStreak", ET_Ignore, Param_Cell, Param_Cell, Param_Float );
g_hForwardAlarmTriggered = CreateGlobalForward("OnCarAlarmTriggered", ET_Ignore, Param_Cell, Param_Cell, Param_Cell );
g_bLateLoad = late;
return APLRes_Success;
}
public OnPluginStart()
{
// hooks
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("scavenge_round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post);
HookEvent("player_hurt", Event_PlayerHurt, EventHookMode_Pre);
HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre);
HookEvent("ability_use", Event_AbilityUse, EventHookMode_Post);
HookEvent("lunge_pounce", Event_LungePounce, EventHookMode_Post);
HookEvent("player_shoved", Event_PlayerShoved, EventHookMode_Post);
HookEvent("player_jump", Event_PlayerJumped, EventHookMode_Post);
HookEvent("player_jump_apex", Event_PlayerJumpApex, EventHookMode_Post);
HookEvent("player_now_it", Event_PlayerBoomed, EventHookMode_Post);
HookEvent("boomer_exploded", Event_BoomerExploded, EventHookMode_Post);
//HookEvent("infected_hurt", Event_InfectedHurt, EventHookMode_Post);
HookEvent("witch_spawn", Event_WitchSpawned, EventHookMode_Post);
HookEvent("witch_killed", Event_WitchKilled, EventHookMode_Post);
HookEvent("witch_harasser_set", Event_WitchHarasserSet, EventHookMode_Post);
HookEvent("tongue_grab", Event_TongueGrab, EventHookMode_Post);
HookEvent("tongue_pull_stopped", Event_TonguePullStopped, EventHookMode_Post);
HookEvent("choke_start", Event_ChokeStart, EventHookMode_Post);
HookEvent("choke_stopped", Event_ChokeStop, EventHookMode_Post);
HookEvent("jockey_ride", Event_JockeyRide, EventHookMode_Post);
HookEvent("charger_carry_start", Event_ChargeCarryStart, EventHookMode_Post);
HookEvent("charger_carry_end", Event_ChargeCarryEnd, EventHookMode_Post);
HookEvent("charger_impact", Event_ChargeImpact, EventHookMode_Post);
HookEvent("charger_pummel_start", Event_ChargePummelStart, EventHookMode_Post);
HookEvent("player_incapacitated_start", Event_IncapStart, EventHookMode_Post);
HookEvent("triggered_car_alarm", Event_CarAlarmGoesOff, EventHookMode_Post);
// version cvar
CreateConVar( "sm_skill_detect_version", PLUGIN_VERSION, "Skill detect plugin version.", FCVAR_NONE|FCVAR_NOTIFY|FCVAR_REPLICATED|FCVAR_DONTRECORD );
// cvars: config
g_hCvarReport = CreateConVar( "sm_skill_report_enable" , "0", "Whether to report in chat (see sm_skill_report_flags).", FCVAR_NONE, true, 0.0, true, 1.0 );
g_hCvarReportFlags = CreateConVar( "sm_skill_report_flags" , REP_DEFAULT, "What to report skeets in chat (bitflags: 1,2:skeets/hurt; 4,8:level/chip; 16,32:crown/draw; 64,128:cut/selfclear, ... ).", FCVAR_NONE, true, 0.0 );
g_hCvarAllowMelee = CreateConVar( "sm_skill_skeet_allowmelee", "1", "Whether to count/forward melee skeets.", FCVAR_NONE, true, 0.0, true, 1.0 );
g_hCvarAllowSniper = CreateConVar( "sm_skill_skeet_allowsniper", "1", "Whether to count/forward sniper/magnum headshots as skeets.", FCVAR_NONE, true, 0.0, true, 1.0 );
g_hCvarAllowGLSkeet = CreateConVar( "sm_skill_skeet_allowgl", "1", "Whether to count/forward direct GL hits as skeets.", FCVAR_NONE, true, 0.0, true, 1.0 );
g_hCvarDrawCrownThresh = CreateConVar( "sm_skill_drawcrown_damage", "500", "How much damage a survivor must at least do in the final shot for it to count as a drawcrown.", FCVAR_NONE, true, 0.0, false );
g_hCvarSelfClearThresh = CreateConVar( "sm_skill_selfclear_damage", "200", "How much damage a survivor must at least do to a smoker for him to count as self-clearing.", FCVAR_NONE, true, 0.0, false );
g_hCvarHunterDPThresh = CreateConVar( "sm_skill_hunterdp_height", "400", "Minimum height of hunter pounce for it to count as a DP.", FCVAR_NONE, true, 0.0, false );
g_hCvarJockeyDPThresh = CreateConVar( "sm_skill_jockeydp_height", "300", "How much height distance a jockey must make for his 'DP' to count as a reportable highpounce.", FCVAR_NONE, true, 0.0, false );
g_hCvarHideFakeDamage = CreateConVar( "sm_skill_hidefakedamage", "0", "If set, any damage done that exceeds the health of a victim is hidden in reports.", FCVAR_NONE, true, 0.0, true, 1.0 );
g_hCvarDeathChargeHeight = CreateConVar("sm_skill_deathcharge_height","400", "How much height distance a charger must take its victim for a deathcharge to be reported.", FCVAR_NONE, true, 0.0, false );
g_hCvarInstaTime = CreateConVar( "sm_skill_instaclear_time", "0.75", "A clear within this time (in seconds) counts as an insta-clear.", FCVAR_NONE, true, 0.0, false );
g_hCvarBHopMinStreak = CreateConVar( "sm_skill_bhopstreak", "3", "The lowest bunnyhop streak that will be reported.", FCVAR_NONE, true, 0.0, false );
g_hCvarBHopMinInitSpeed = CreateConVar( "sm_skill_bhopinitspeed", "150", "The minimal speed of the first jump of a bunnyhopstreak (0 to allow 'hops' from standstill).", FCVAR_NONE, true, 0.0, false );
g_hCvarBHopContSpeed = CreateConVar( "sm_skill_bhopkeepspeed", "300", "The minimal speed at which hops are considered succesful even if not speed increase is made.", FCVAR_NONE, true, 0.0, false );
// cvars: built in
g_hCvarPounceInterrupt = FindConVar("z_pounce_damage_interrupt");
HookConVarChange(g_hCvarPounceInterrupt, CvarChange_PounceInterrupt);
g_iPounceInterrupt = GetConVarInt(g_hCvarPounceInterrupt);
g_hCvarChargerHealth = FindConVar("z_charger_health");
g_hCvarWitchHealth = FindConVar("z_witch_health");
g_hCvarMaxPounceDistance = FindConVar("z_pounce_damage_range_max");
g_hCvarMinPounceDistance = FindConVar("z_pounce_damage_range_min");
g_hCvarMaxPounceDamage = FindConVar("z_hunter_max_pounce_bonus_damage");
if ( g_hCvarMaxPounceDistance == INVALID_HANDLE ) { g_hCvarMaxPounceDistance = CreateConVar( "z_pounce_damage_range_max", "1000.0", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); }
if ( g_hCvarMinPounceDistance == INVALID_HANDLE ) { g_hCvarMinPounceDistance = CreateConVar( "z_pounce_damage_range_min", "300.0", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); }
if ( g_hCvarMaxPounceDamage == INVALID_HANDLE ) { g_hCvarMaxPounceDamage = CreateConVar( "z_hunter_max_pounce_bonus_damage", "49", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); }
// tries
g_hTrieWeapons = CreateTrie();
SetTrieValue(g_hTrieWeapons, "hunting_rifle", WPTYPE_SNIPER);
SetTrieValue(g_hTrieWeapons, "sniper_military", WPTYPE_SNIPER);
SetTrieValue(g_hTrieWeapons, "sniper_awp", WPTYPE_SNIPER);
SetTrieValue(g_hTrieWeapons, "sniper_scout", WPTYPE_SNIPER);
SetTrieValue(g_hTrieWeapons, "pistol_magnum", WPTYPE_MAGNUM);
SetTrieValue(g_hTrieWeapons, "grenade_launcher_projectile", WPTYPE_GL);
g_hTrieEntityCreated = CreateTrie();
SetTrieValue(g_hTrieEntityCreated, "tank_rock", OEC_TANKROCK);
SetTrieValue(g_hTrieEntityCreated, "witch", OEC_WITCH);
SetTrieValue(g_hTrieEntityCreated, "trigger_hurt", OEC_TRIGGER);
SetTrieValue(g_hTrieEntityCreated, "prop_car_alarm", OEC_CARALARM);
SetTrieValue(g_hTrieEntityCreated, "prop_car_glass", OEC_CARGLASS);
g_hTrieAbility = CreateTrie();
SetTrieValue(g_hTrieAbility, "ability_lunge", ABL_HUNTERLUNGE);
SetTrieValue(g_hTrieAbility, "ability_throw", ABL_ROCKTHROW);
g_hWitchTrie = CreateTrie();
g_hRockTrie = CreateTrie();
g_hCarTrie = CreateTrie();
if ( g_bLateLoad )
{
for ( new client = 1; client <= MaxClients; client++ )
{
if ( IS_VALID_INGAME(client) )
{
SDKHook( client, SDKHook_OnTakeDamage, OnTakeDamageByWitch );
}
}
}
AutoExecConfig(true, "l4d2_skill_detect");
}
public CvarChange_PounceInterrupt( Handle:convar, const String:oldValue[], const String:newValue[] )
{
g_iPounceInterrupt = GetConVarInt(convar);
}
public OnClientPostAdminCheck(client)
{
SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamageByWitch);
}
public OnClientDisconnect(client)
{
SDKUnhook(client, SDKHook_OnTakeDamage, OnTakeDamageByWitch);
}
/*
Tracking
--------
*/
public Action: Event_RoundStart( Handle:event, const String:name[], bool:dontBroadcast )
{
g_iRocksBeingThrownCount = 0;
for ( new i = 1; i <= MaxClients; i++ )
{
g_bIsHopping[i] = false;
for ( new j = 1; j <= MaxClients; j++ )
{
g_fVictimLastShove[i][j] = 0.0;
}
}
}
public Action: Event_RoundEnd( Handle:event, const String:name[], bool:dontBroadcast )
{
// clean trie, new cars will be created
ClearTrie(g_hCarTrie);
}
public Action: Event_PlayerHurt( Handle:event, const String:name[], bool:dontBroadcast )
{
new victim = GetClientOfUserId(GetEventInt(event, "userid"));
new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
new zClass;
new damage = GetEventInt(event, "dmg_health");
new damagetype = GetEventInt(event, "type");
if ( IS_VALID_INFECTED(victim) )
{
zClass = GetEntProp(victim, Prop_Send, "m_zombieClass");
new health = GetEventInt(event, "health");
new hitgroup = GetEventInt(event, "hitgroup");
if ( damage < 1 ) { return Plugin_Continue; }
switch ( zClass )
{
case ZC_HUNTER:
{
// if it's not a survivor doing the work, only get the remaining health
if ( !IS_VALID_SURVIVOR(attacker) )
{
g_iHunterLastHealth[victim] = health;
return Plugin_Continue;
}
// if the damage done is greater than the health we know the hunter to have remaining, reduce the damage done
if ( g_iHunterLastHealth[victim] > 0 && damage > g_iHunterLastHealth[victim] )
{
damage = g_iHunterLastHealth[victim];
g_iHunterOverkill[victim] = g_iHunterLastHealth[victim] - damage;
g_iHunterLastHealth[victim] = 0;
}
/*
handle old shotgun blast: too long ago? not the same blast
*/
if ( g_iHunterShotDmg[victim][attacker] > 0 && (GetGameTime() - g_fHunterShotStart[victim][attacker]) > SHOTGUN_BLAST_TIME )
{
g_fHunterShotStart[victim][attacker] = 0.0;
}
/*
m_isAttemptingToPounce is set to 0 here if the hunter is actually skeeted
so the g_fHunterTracePouncing[victim] value indicates when the hunter was last seen pouncing in traceattack
(should be DIRECTLY before this event for every shot).
*/
new bool: isPouncing = bool:(
GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") ||
g_fHunterTracePouncing[victim] != 0.0 && ( GetGameTime() - g_fHunterTracePouncing[victim] ) < 0.001
);
if ( isPouncing )
{
if ( damagetype & DMG_BUCKSHOT )
{
// first pellet hit?
if ( g_fHunterShotStart[victim][attacker] == 0.0 )
{
// new shotgun blast
g_fHunterShotStart[victim][attacker] = GetGameTime();
g_fHunterLastShot[victim] = g_fHunterShotStart[victim][attacker];
}
g_iHunterShotDmg[victim][attacker] += damage;
g_iHunterShotDmgTeam[victim] += damage;
if ( health == 0 ) {
g_bHunterKilledPouncing[victim] = true;
}
}
else if ( damagetype & (DMG_BLAST | DMG_PLASMA) && health == 0 )
{
// direct GL hit?
/*
direct hit is DMG_BLAST | DMG_PLASMA
indirect hit is DMG_AIRBOAT
*/
decl String: weaponB[32];
new strWeaponType: weaponTypeB;
GetEventString(event, "weapon", weaponB, sizeof(weaponB));
if ( GetTrieValue(g_hTrieWeapons, weaponB, weaponTypeB) && weaponTypeB == WPTYPE_GL )
{
if ( GetConVarBool(g_hCvarAllowGLSkeet) ) {
HandleSkeet( attacker, victim, false, false, true );
}
}
}
else if ( damagetype & DMG_BULLET &&
health == 0 &&
hitgroup == HITGROUP_HEAD
) {
// headshot with bullet based weapon (only single shots) -- only snipers
decl String: weaponA[32];
new strWeaponType: weaponTypeA;
GetEventString(event, "weapon", weaponA, sizeof(weaponA));
if ( GetTrieValue(g_hTrieWeapons, weaponA, weaponTypeA) &&
( weaponTypeA == WPTYPE_SNIPER ||
weaponTypeA == WPTYPE_MAGNUM )
) {
if ( damage >= g_iPounceInterrupt )
{
g_iHunterShotDmgTeam[victim] = 0;
if ( GetConVarBool(g_hCvarAllowSniper) ) {
HandleSkeet( attacker, victim, false, true );
}
ResetHunter(victim);
}
else
{
// hurt skeet
if ( GetConVarBool(g_hCvarAllowSniper) ) {
HandleNonSkeet( attacker, victim, damage, ( g_iHunterOverkill[victim] + g_iHunterShotDmgTeam[victim] > g_iPounceInterrupt ), false, true );
}
ResetHunter(victim);
}
}
// already handled hurt skeet above
//g_bHunterKilledPouncing[victim] = true;
}
else if ( damagetype & DMG_SLASH || damagetype & DMG_CLUB )
{
// melee skeet
if ( damage >= g_iPounceInterrupt )
{
g_iHunterShotDmgTeam[victim] = 0;
if ( GetConVarBool(g_hCvarAllowMelee) ) {
HandleSkeet( attacker, victim, true );
}
ResetHunter(victim);
//g_bHunterKilledPouncing[victim] = true;
}
else if ( health == 0 )
{
// hurt skeet (always overkill)
if ( GetConVarBool(g_hCvarAllowMelee) ) {
HandleNonSkeet( attacker, victim, damage, true, true, false );
}
ResetHunter(victim);
}
}
}
else if ( health == 0 )
{
// make sure we don't mistake non-pouncing hunters as 'not skeeted'-warnable
g_bHunterKilledPouncing[victim] = false;
}
// store last health seen for next damage event
g_iHunterLastHealth[victim] = health;
}
case ZC_CHARGER:
{
if ( IS_VALID_SURVIVOR(attacker) )
{
// check for levels
if ( health == 0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) )
{
new iChargeHealth = GetConVarInt(g_hCvarChargerHealth);
new abilityEnt = GetEntPropEnt( victim, Prop_Send, "m_customAbility" );
if ( IsValidEntity(abilityEnt) && GetEntProp(abilityEnt, Prop_Send, "m_isCharging") )
{
// fix fake damage?
if ( GetConVarBool(g_hCvarHideFakeDamage) )
{
damage = iChargeHealth - g_iChargerHealth[victim];
}
// charger was killed, was it a full level?
if ( damage > (iChargeHealth * 0.65) ) {
HandleLevel( attacker, victim );
}
else {
HandleLevelHurt( attacker, victim, damage );
}
}
}
}
// store health for next damage it takes
if ( health > 0 )
{
g_iChargerHealth[victim] = health;
}
}
case ZC_SMOKER:
{
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
g_iSmokerVictimDamage[victim] += damage;
}
}
}
else if ( IS_VALID_INFECTED(attacker) )
{
zClass = GetEntProp(attacker, Prop_Send, "m_zombieClass");
switch ( zClass )
{
case ZC_HUNTER:
{
// a hunter pounce landing is DMG_CRUSH
if ( damagetype & DMG_CRUSH ) {
g_iPounceDamage[attacker] = damage;
}
}
case ZC_TANK:
{
new String: weapon[10];
GetEventString(event, "weapon", weapon, sizeof(weapon));
if ( StrEqual(weapon, "tank_rock") )
{
// find rock entity through tank
if ( g_iTankRock[attacker] )
{
// remember that the rock wasn't shot
decl String:rock_key[10];
FormatEx(rock_key, sizeof(rock_key), "%x", g_iTankRock[attacker]);
new rock_array[3];
rock_array[rckDamage] = -1;
SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true);
}
if ( IS_VALID_SURVIVOR(victim) )
{
HandleRockEaten( attacker, victim );
}
}
return Plugin_Continue;
}
}
}
// check for deathcharge flags
if ( IS_VALID_SURVIVOR(victim) )
{
// debug
if ( damagetype & DMG_DROWN || damagetype & DMG_FALL ) {
g_iVictimMapDmg[victim] += damage;
}
if ( damagetype & DMG_DROWN && damage >= MIN_DC_TRIGGER_DMG )
{
g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_HURTLOTS;
}
else if ( damagetype & DMG_FALL && damage >= MIN_DC_FALL_DMG )
{
g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_HURTLOTS;
}
}
return Plugin_Continue;
}
public Action: Event_PlayerSpawn( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId(GetEventInt(event, "userid"));
if ( !IS_VALID_INFECTED(client) ) { return Plugin_Continue; }
new zClass = GetEntProp(client, Prop_Send, "m_zombieClass");
g_fSpawnTime[client] = GetGameTime();
g_fPinTime[client][0] = 0.0;
g_fPinTime[client][1] = 0.0;
switch ( zClass )
{
case ZC_BOOMER:
{
g_bBoomerHitSomebody[client] = false;
g_iBoomerGotShoved[client] = 0;
}
case ZC_SMOKER:
{
g_bSmokerClearCheck[client] = false;
g_iSmokerVictim[client] = 0;
g_iSmokerVictimDamage[client] = 0;
}
case ZC_HUNTER:
{
SDKHook(client, SDKHook_TraceAttack, TraceAttack_Hunter);
g_fPouncePosition[client][0] = 0.0;
g_fPouncePosition[client][1] = 0.0;
g_fPouncePosition[client][2] = 0.0;
}
case ZC_JOCKEY:
{
SDKHook(client, SDKHook_TraceAttack, TraceAttack_Jockey);
g_fPouncePosition[client][0] = 0.0;
g_fPouncePosition[client][1] = 0.0;
g_fPouncePosition[client][2] = 0.0;
}
case ZC_CHARGER:
{
SDKHook(client, SDKHook_TraceAttack, TraceAttack_Charger);
g_iChargerHealth[client] = GetConVarInt(g_hCvarChargerHealth);
}
}
return Plugin_Continue;
}
// player about to get incapped
public Action: Event_IncapStart( Handle:event, const String:name[], bool:dontBroadcast )
{
// test for deathcharges
new client = GetClientOfUserId( GetEventInt(event, "userid") );
//new attacker = GetClientOfUserId( GetEventInt(event, "attacker") );
new attackent = GetEventInt(event, "attackerentid");
new dmgtype = GetEventInt(event, "type");
new String: classname[24];
new strOEC: classnameOEC;
if ( IsValidEntity(attackent) ) {
GetEdictClassname(attackent, classname, sizeof(classname));
if ( GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC)) {
g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_TRIGGER;
}
}
new Float: flow = GetSurvivorDistance(client);
//PrintDebug( 3, "Incap Pre on [%N]: attk: %i / %i (%s) - dmgtype: %i - flow: %.1f", client, attacker, attackent, classname, dmgtype, flow );
// drown is damage type
if ( dmgtype & DMG_DROWN )
{
g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_DROWN;
}
if ( flow < WEIRD_FLOW_THRESH )
{
g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_WEIRDFLOW;
}
}
// trace attacks on hunters
public Action: TraceAttack_Hunter (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup)
{
// track pinning
g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_pounceVictim");
if ( !IS_VALID_SURVIVOR(attacker) || !IsValidEdict(inflictor) ) { return; }
// track flight
if ( GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") )
{
g_fHunterTracePouncing[victim] = GetGameTime();
}
else
{
g_fHunterTracePouncing[victim] = 0.0;
}
}
public Action: TraceAttack_Charger (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup)
{
// track pinning
new victimA = GetEntPropEnt(victim, Prop_Send, "m_carryVictim");
if ( victimA != -1 ) {
g_iSpecialVictim[victim] = victimA;
} else {
g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_pummelVictim");
}
}
public Action: TraceAttack_Jockey (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup)
{
// track pinning
g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim");
}
public Action: Event_PlayerDeath( Handle:hEvent, const String:name[], bool:dontBroadcast )
{
new victim = GetClientOfUserId( GetEventInt(hEvent, "userid") );
new attacker = GetClientOfUserId( GetEventInt(hEvent, "attacker") );
if ( IS_VALID_INFECTED(victim) )
{
new zClass = GetEntProp(victim, Prop_Send, "m_zombieClass");
switch ( zClass )
{
case ZC_HUNTER:
{
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
if ( g_iHunterShotDmgTeam[victim] > 0 && g_bHunterKilledPouncing[victim] )
{
// skeet?
if ( g_iHunterShotDmgTeam[victim] > g_iHunterShotDmg[victim][attacker] &&
g_iHunterShotDmgTeam[victim] >= g_iPounceInterrupt
) {
// team skeet
HandleSkeet( -2, victim );
}
else if ( g_iHunterShotDmg[victim][attacker] >= g_iPounceInterrupt )
{
// single player skeet
HandleSkeet( attacker, victim );
}
else if ( g_iHunterOverkill[victim] > 0 )
{
// overkill? might've been a skeet, if it wasn't on a hurt hunter (only for shotguns)
HandleNonSkeet( attacker, victim, g_iHunterShotDmgTeam[victim], ( g_iHunterOverkill[victim] + g_iHunterShotDmgTeam[victim] > g_iPounceInterrupt ) );
}
else
{
// not a skeet at all
HandleNonSkeet( attacker, victim, g_iHunterShotDmg[victim][attacker] );
}
}
else {
// check whether it was a clear
if ( g_iSpecialVictim[victim] > 0 )
{
HandleClear( attacker, victim, g_iSpecialVictim[victim],
ZC_HUNTER,
( GetGameTime() - g_fPinTime[victim][0]),
-1.0
);
}
}
ResetHunter(victim);
}
case ZC_SMOKER:
{
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
if ( g_bSmokerClearCheck[victim] &&
g_iSmokerVictim[victim] == attacker &&
g_iSmokerVictimDamage[victim] >= GetConVarInt(g_hCvarSelfClearThresh)
) {
HandleSmokerSelfClear( attacker, victim );
}
else
{
g_bSmokerClearCheck[victim] = false;
g_iSmokerVictim[victim] = 0;
}
}
case ZC_JOCKEY:
{
// check whether it was a clear
if ( g_iSpecialVictim[victim] > 0 )
{
HandleClear( attacker, victim, g_iSpecialVictim[victim],
ZC_JOCKEY,
( GetGameTime() - g_fPinTime[victim][0]),
-1.0
);
}
}
case ZC_CHARGER:
{
// is it someone carrying a survivor (that might be DC'd)?
// switch charge victim to 'impact' check (reset checktime)
if ( IS_VALID_INGAME(g_iChargeVictim[victim]) ) {
g_fChargeTime[ g_iChargeVictim[victim] ] = GetGameTime();
}
// check whether it was a clear
if ( g_iSpecialVictim[victim] > 0 )
{
HandleClear( attacker, victim, g_iSpecialVictim[victim],
ZC_CHARGER,
(g_fPinTime[victim][1] > 0.0) ? ( GetGameTime() - g_fPinTime[victim][1]) : -1.0,
( GetGameTime() - g_fPinTime[victim][0])
);
}
}
}
}
else if ( IS_VALID_SURVIVOR(victim) )
{
// check for deathcharges
//new atkent = GetEventInt(hEvent, "attackerentid");
new dmgtype = GetEventInt(hEvent, "type");
//PrintDebug( 3, "Died [%N]: attk: %i / %i - dmgtype: %i", victim, attacker, atkent, dmgtype );
if ( dmgtype & DMG_FALL)
{
g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_FALL;
}
else if ( IS_VALID_INFECTED(attacker) && attacker != g_iVictimCharger[victim] )
{
// if something other than the charger killed them, remember (not a DC)
g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_KILLEDBYOTHER;
}
}
return Plugin_Continue;
}
public Action: Event_PlayerShoved( Handle:event, const String:name[], bool:dontBroadcast )
{
new victim = GetClientOfUserId(GetEventInt(event, "userid"));
new attacker = GetClientOfUserId(GetEventInt(event, "attacker"));
//PrintDebug(1, "Shove from %i on %i", attacker, victim);
if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(victim) ) { return Plugin_Continue; }
new zClass = GetEntProp(victim, Prop_Send, "m_zombieClass");
//PrintDebug(1, " --> Shove from %N on %N (class: %i) -- (last shove time: %.2f / %.2f)", attacker, victim, zClass, g_fVictimLastShove[victim][attacker], ( GetGameTime() - g_fVictimLastShove[victim][attacker] ) );
// track on boomers
if ( zClass == ZC_BOOMER )
{
g_iBoomerGotShoved[victim]++;
}
else {
// check for clears
switch ( zClass )
{
case ZC_HUNTER: {
if ( GetEntPropEnt(victim, Prop_Send, "m_pounceVictim") > 0 )
{
HandleClear( attacker, victim, GetEntPropEnt(victim, Prop_Send, "m_pounceVictim"),
ZC_HUNTER,
( GetGameTime() - g_fPinTime[victim][0]),
-1.0,
true
);
}
}
case ZC_JOCKEY: {
if ( GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim") > 0 )
{
HandleClear( attacker, victim, GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim"),
ZC_JOCKEY,
( GetGameTime() - g_fPinTime[victim][0]),
-1.0,
true
);
}
}
}
}
if ( g_fVictimLastShove[victim][attacker] == 0.0 || ( GetGameTime() - g_fVictimLastShove[victim][attacker] ) >= SHOVE_TIME )
{
if ( GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") )
{
HandleDeadstop( attacker, victim );
}
HandleShove( attacker, victim, zClass );
g_fVictimLastShove[victim][attacker] = GetGameTime();
}
// check for shove on smoker by pull victim
if ( g_iSmokerVictim[victim] == attacker )
{
g_bSmokerShoved[victim] = true;
}
//PrintDebug(0, "shove by %i on %i", attacker, victim );
return Plugin_Continue;
}
public Action: Event_LungePounce( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
g_fPinTime[client][0] = GetGameTime();
// clear hunter-hit stats (not skeeted)
ResetHunter(client);
// check if it was a DP
// ignore if no real pounce start pos
if ( g_fPouncePosition[client][0] == 0.0
&& g_fPouncePosition[client][1] == 0.0
&& g_fPouncePosition[client][2] == 0.0
) {
return Plugin_Continue;
}
new Float: endPos[3];
GetClientAbsOrigin( client, endPos );
new Float: fHeight = g_fPouncePosition[client][2] - endPos[2];
// from pounceannounce:
// distance supplied isn't the actual 2d vector distance needed for damage calculation. See more about it at
// http://forums.alliedmods.net/showthread.php?t=93207
new Float: fMin = GetConVarFloat(g_hCvarMinPounceDistance);
new Float: fMax = GetConVarFloat(g_hCvarMaxPounceDistance);
new Float: fMaxDmg = GetConVarFloat(g_hCvarMaxPounceDamage);
// calculate 2d distance between previous position and pounce position
new distance = RoundToNearest( GetVectorDistance(g_fPouncePosition[client], endPos) );
// get damage using hunter damage formula
// check if this is accurate, seems to differ from actual damage done!
new Float: fDamage = ( ( (float(distance) - fMin) / (fMax - fMin) ) * fMaxDmg ) + 1.0;
// apply bounds
if (fDamage < 0.0) {
fDamage = 0.0;
} else if (fDamage > fMaxDmg + 1.0) {
fDamage = fMaxDmg + 1.0;
}
new Handle: pack = CreateDataPack();
WritePackCell( pack, client );
WritePackCell( pack, victim );
WritePackFloat( pack, fDamage );
WritePackFloat( pack, fHeight );
CreateTimer( 0.05, Timer_HunterDP, pack );
return Plugin_Continue;
}
public Action: Timer_HunterDP( Handle:timer, Handle:pack )
{
ResetPack( pack );
new client = ReadPackCell( pack );
new victim = ReadPackCell( pack );
new Float: fDamage = ReadPackFloat( pack );
new Float: fHeight = ReadPackFloat( pack );
CloseHandle( pack );
HandleHunterDP( client, victim, g_iPounceDamage[client], fDamage, fHeight );
}
public Action: Event_PlayerJumped( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
if ( IS_VALID_INFECTED(client) )
{
new zClass = GetEntProp(client, Prop_Send, "m_zombieClass");
if ( zClass != ZC_JOCKEY ) { return Plugin_Continue; }
// where did jockey jump from?
GetClientAbsOrigin( client, g_fPouncePosition[client] );
}
else if ( IS_VALID_SURVIVOR(client) )
{
// could be the start or part of a hopping streak
new Float: fPos[3], Float: fVel[3];
GetClientAbsOrigin( client, fPos );
GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel );
fVel[2] = 0.0; // safeguard
new Float: fLengthNew, Float: fLengthOld;
fLengthNew = GetVectorLength(fVel);
g_bHopCheck[client] = false;
if ( !g_bIsHopping[client] )
{
if ( fLengthNew >= GetConVarFloat(g_hCvarBHopMinInitSpeed) )
{
// starting potential hop streak
g_fHopTopVelocity[client] = fLengthNew;
g_bIsHopping[client] = true;
g_iHops[client] = 0;
}
}
else
{
// check for hopping streak
fLengthOld = GetVectorLength(g_fLastHop[client]);
// if they picked up speed, count it as a hop, otherwise, we're done hopping
if ( fLengthNew - fLengthOld > HOP_ACCEL_THRESH || fLengthNew >= GetConVarFloat(g_hCvarBHopContSpeed) )
{
g_iHops[client]++;
// this should always be the case...
if ( fLengthNew > g_fHopTopVelocity[client] )
{
g_fHopTopVelocity[client] = fLengthNew;
}
//PrintToChat( client, "bunnyhop %i: speed: %.1f / increase: %.1f", g_iHops[client], fLengthNew, fLengthNew - fLengthOld );
}
else
{
g_bIsHopping[client] = false;
if ( g_iHops[client] )
{
HandleBHopStreak( client, g_iHops[client], g_fHopTopVelocity[client] );
g_iHops[client] = 0;
}
}
}
g_fLastHop[client][0] = fVel[0];
g_fLastHop[client][1] = fVel[1];
g_fLastHop[client][2] = fVel[2];
if ( g_iHops[client] != 0 )
{
// check when the player returns to the ground
CreateTimer( HOP_CHECK_TIME, Timer_CheckHop, client, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE );
}
}
return Plugin_Continue;
}
public Action: Timer_CheckHop (Handle:timer, any:client)
{
// player back to ground = end of hop (streak)?
if ( !IS_VALID_INGAME(client) || !IsPlayerAlive(client) )
{
// streak stopped by dying / teamswitch / disconnect?
return Plugin_Stop;
}
else if ( GetEntityFlags(client) & FL_ONGROUND )
{
new Float: fVel[3];
GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel );
fVel[2] = 0.0; // safeguard
//PrintToChatAll("grounded %i: vel length: %.1f", client, GetVectorLength(fVel) );
g_bHopCheck[client] = true;
CreateTimer( HOPEND_CHECK_TIME, Timer_CheckHopStreak, client, TIMER_FLAG_NO_MAPCHANGE );
return Plugin_Stop;
}
return Plugin_Continue;
}
public Action: Timer_CheckHopStreak (Handle:timer, any:client)
{
if ( !IS_VALID_INGAME(client) || !IsPlayerAlive(client) ) { return Plugin_Continue; }
// check if we have any sort of hop streak, and report
if ( g_bHopCheck[client] && g_iHops[client] )
{
HandleBHopStreak( client, g_iHops[client], g_fHopTopVelocity[client] );
g_bIsHopping[client] = false;
g_iHops[client] = 0;
g_fHopTopVelocity[client] = 0.0;
}
g_bHopCheck[client] = false;
return Plugin_Continue;
}
public Action: Event_PlayerJumpApex( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
if ( g_bIsHopping[client] )
{
new Float: fVel[3];
GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel );
fVel[2] = 0.0;
new Float: fLength = GetVectorLength(fVel);
if ( fLength > g_fHopTopVelocity[client] )
{
g_fHopTopVelocity[client] = fLength;
}
}
}
public Action: Event_JockeyRide( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
if ( !IS_VALID_INFECTED(client) || !IS_VALID_SURVIVOR(victim) ) { return Plugin_Continue; }
g_fPinTime[client][0] = GetGameTime();
// minimum distance travelled?
// ignore if no real pounce start pos
if ( g_fPouncePosition[client][0] == 0.0 && g_fPouncePosition[client][1] == 0.0 && g_fPouncePosition[client][2] == 0.0 ) { return Plugin_Continue; }
new Float: endPos[3];
GetClientAbsOrigin( client, endPos );
new Float: fHeight = g_fPouncePosition[client][2] - endPos[2];
//PrintToChatAll("jockey height: %.3f", fHeight);
// (high) pounce
HandleJockeyDP( client, victim, fHeight );
return Plugin_Continue;
}
public Action: Event_AbilityUse( Handle:event, const String:name[], bool:dontBroadcast )
{
// track hunters pouncing
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new String: abilityName[64];
GetEventString( event, "ability", abilityName, sizeof(abilityName) );
if ( !IS_VALID_INGAME(client) ) { return Plugin_Continue; }
new strAbility: ability;
if ( !GetTrieValue(g_hTrieAbility, abilityName, ability) ) { return Plugin_Continue; }
switch ( ability )
{
case ABL_HUNTERLUNGE:
{
// hunter started a pounce
ResetHunter(client);
GetClientAbsOrigin( client, g_fPouncePosition[client] );
}
case ABL_ROCKTHROW:
{
// tank throws rock
g_iRocksBeingThrown[g_iRocksBeingThrownCount] = client;
// safeguard
if ( g_iRocksBeingThrownCount < 9 ) { g_iRocksBeingThrownCount++; }
}
}
return Plugin_Continue;
}
// charger carrying
public Action: Event_ChargeCarryStart( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
if ( !IS_VALID_INFECTED(client) ) { return; }
PrintDebug(0, "Charge carry start: %i - %i -- time: %.2f", client, victim, GetGameTime() );
g_fChargeTime[client] = GetGameTime();
g_fPinTime[client][0] = g_fChargeTime[client];
g_fPinTime[client][1] = 0.0;
if ( !IS_VALID_SURVIVOR(victim) ) { return; }
g_iChargeVictim[client] = victim; // store who we're carrying (as long as this is set, it's not considered an impact charge flight)
g_iVictimCharger[victim] = client; // store who's charging whom
g_iVictimFlags[victim] = VICFLG_CARRIED; // reset flags for checking later - we know only this now
g_fChargeTime[victim] = g_fChargeTime[client];
g_iVictimMapDmg[victim] = 0;
GetClientAbsOrigin( victim, g_fChargeVictimPos[victim] );
//CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, client, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE );
CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, victim, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE );
}
public Action: Event_ChargeImpact( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
if ( !IS_VALID_INFECTED(client) || !IS_VALID_SURVIVOR(victim) ) { return; }
// remember how many people the charger bumped into, and who, and where they were
GetClientAbsOrigin( victim, g_fChargeVictimPos[victim] );
g_iVictimCharger[victim] = client; // store who we've bumped up
g_iVictimFlags[victim] = 0; // reset flags for checking later
g_fChargeTime[victim] = GetGameTime(); // store time per victim, for impacts
g_iVictimMapDmg[victim] = 0;
CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, victim, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE );
}
public Action: Event_ChargePummelStart( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
if ( !IS_VALID_INFECTED(client) ) { return; }
g_fPinTime[client][1] = GetGameTime();
}
public Action: Event_ChargeCarryEnd( Handle:event, const String:name[], bool:dontBroadcast )
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
if ( client < 1 || client > MaxClients ) { return; }
g_fPinTime[client][1] = GetGameTime();
// delay so we can check whether charger died 'mid carry'
CreateTimer( 0.1, Timer_ChargeCarryEnd, client, TIMER_FLAG_NO_MAPCHANGE );
}
public Action: Timer_ChargeCarryEnd( Handle:timer, any:client )
{
// set charge time to 0 to avoid deathcharge timer continuing
g_iChargeVictim[client] = 0; // unset this so the repeated timer knows to stop for an ongroundcheck
}
public Action: Timer_ChargeCheck( Handle:timer, any:client )
{
// if something went wrong with the survivor or it was too long ago, forget about it
if ( !IS_VALID_SURVIVOR(client) || !g_iVictimCharger[client] || g_fChargeTime[client] == 0.0 || ( GetGameTime() - g_fChargeTime[client]) > MAX_CHARGE_TIME )
{
return Plugin_Stop;
}
// we're done checking if either the victim reached the ground, or died
if ( !IsPlayerAlive(client) )
{
// player died (this was .. probably.. a death charge)
g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_AIRDEATH;
// check conditions now
CreateTimer( 0.0, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE );
return Plugin_Stop;
}
else if ( GetEntityFlags(client) & FL_ONGROUND && g_iChargeVictim[ g_iVictimCharger[client] ] != client )
{
// survivor reached the ground and didn't die (yet)
// the client-check condition checks whether the survivor is still being carried by the charger
// (in which case it doesn't matter that they're on the ground)
// check conditions with small delay (to see if they still die soon)
CreateTimer( CHARGE_END_CHECK, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE );
return Plugin_Stop;
}
return Plugin_Continue;
}
public Action: Timer_DeathChargeCheck( Handle:timer, any:client )
{
if ( !IS_VALID_INGAME(client) ) { return; }
// check conditions.. if flags match up, it's a DC
PrintDebug( 3, "Checking charge victim: %i - %i - flags: %i (alive? %i)", g_iVictimCharger[client], client, g_iVictimFlags[client], IsPlayerAlive(client) );
new flags = g_iVictimFlags[client];
if ( !IsPlayerAlive(client) )
{
new Float: pos[3];
GetClientAbsOrigin( client, pos );
new Float: fHeight = g_fChargeVictimPos[client][2] - pos[2];
/*
it's a deathcharge when:
the survivor is dead AND
they drowned/fell AND took enough damage or died in mid-air
AND not killed by someone else
OR is in an unreachable spot AND dropped at least X height
OR took plenty of map damage
old.. need?
fHeight > GetConVarFloat(g_hCvarDeathChargeHeight)
*/
if ( ( ( flags & VICFLG_DROWN || flags & VICFLG_FALL ) &&
( flags & VICFLG_HURTLOTS || flags & VICFLG_AIRDEATH ) ||
( flags & VICFLG_WEIRDFLOW && fHeight >= MIN_FLOWDROPHEIGHT ) ||
g_iVictimMapDmg[client] >= MIN_DC_TRIGGER_DMG
) &&
!( flags & VICFLG_KILLEDBYOTHER )
) {
HandleDeathCharge( g_iVictimCharger[client], client, fHeight, GetVectorDistance(g_fChargeVictimPos[client], pos, false), bool:(flags & VICFLG_CARRIED) );
}
}
else if ( ( flags & VICFLG_WEIRDFLOW || g_iVictimMapDmg[client] >= MIN_DC_RECHECK_DMG ) &&
!(flags & VICFLG_WEIRDFLOWDONE)
) {
// could be incapped and dying more slowly
// flag only gets set on preincap, so don't need to check for incap
g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_WEIRDFLOWDONE;
CreateTimer( CHARGE_END_RECHECK, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE );
}
}
stock ResetHunter(client)
{
g_iHunterShotDmgTeam[client] = 0;
for ( new i=1; i <= MaxClients; i++ )
{
g_iHunterShotDmg[client][i] = 0;
g_fHunterShotStart[client][i] = 0.0;
}
g_iHunterOverkill[client] = 0;
}
// entity creation
public OnEntityCreated ( entity, const String:classname[] )
{
if ( entity < 1 || !IsValidEntity(entity) || !IsValidEdict(entity) ) { return; }
// track infected / witches, so damage on them counts as hits
new strOEC: classnameOEC;
if (!GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC)) { return; }
switch ( classnameOEC )
{
case OEC_TANKROCK:
{
decl String:rock_key[10];
FormatEx(rock_key, sizeof(rock_key), "%x", entity);
new rock_array[3];
// store which tank is throwing what rock
new tank = ShiftTankThrower();
if ( IS_VALID_INGAME(tank) )
{
g_iTankRock[tank] = entity;
rock_array[rckTank] = tank;
}
SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true);
SDKHook(entity, SDKHook_TraceAttack, TraceAttack_Rock);
SDKHook(entity, SDKHook_Touch, OnTouch_Rock);
}
case OEC_CARALARM:
{
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", entity);
SDKHook(entity, SDKHook_OnTakeDamage, OnTakeDamage_Car);
SDKHook(entity, SDKHook_Touch, OnTouch_Car);
SDKHook(entity, SDKHook_Spawn, OnEntitySpawned_CarAlarm);
}
case OEC_CARGLASS:
{
SDKHook(entity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass);
SDKHook(entity, SDKHook_Touch, OnTouch_CarGlass);
//SetTrieValue(g_hCarTrie, car_key, );
SDKHook(entity, SDKHook_Spawn, OnEntitySpawned_CarAlarmGlass);
}
}
}
public OnEntitySpawned_CarAlarm ( entity )
{
if ( !IsValidEntity(entity) ) { return; }
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", entity);
decl String:target[48];
GetEntPropString(entity, Prop_Data, "m_iName", target, sizeof(target));
SetTrieValue( g_hCarTrie, target, entity );
SetTrieValue( g_hCarTrie, car_key, 0 ); // who shot the car?
HookSingleEntityOutput( entity, "OnCarAlarmStart", Hook_CarAlarmStart );
}
public OnEntitySpawned_CarAlarmGlass ( entity )
{
if ( !IsValidEntity(entity) ) { return; }
// glass is parented to a car, link the two through the trie
// find parent and save both
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", entity);
decl String:parent[48];
GetEntPropString(entity, Prop_Data, "m_iParent", parent, sizeof(parent));
new parentEntity;
// find targetname in trie
if ( GetTrieValue(g_hCarTrie, parent, parentEntity ) )
{
// if valid entity, save the parent entity
if ( IsValidEntity(parentEntity) )
{
SetTrieValue( g_hCarTrie, car_key, parentEntity );
decl String:car_key_p[10];
FormatEx(car_key_p, sizeof(car_key_p), "%x_A", parentEntity);
new testEntity;
if ( GetTrieValue(g_hCarTrie, car_key_p, testEntity) )
{
// second glass
FormatEx(car_key_p, sizeof(car_key_p), "%x_B", parentEntity);
}
SetTrieValue( g_hCarTrie, car_key_p, entity );
}
}
}
// entity destruction
public OnEntityDestroyed ( entity )
{
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", entity);
decl rock_array[3];
if ( GetTrieArray(g_hRockTrie, witch_key, rock_array, sizeof(rock_array)) )
{
// tank rock
CreateTimer( ROCK_CHECK_TIME, Timer_CheckRockSkeet, entity );
SDKUnhook(entity, SDKHook_TraceAttack, TraceAttack_Rock);
return;
}
decl witch_array[MAXPLAYERS+DMGARRAYEXT];
if ( GetTrieArray(g_hWitchTrie, witch_key, witch_array, sizeof(witch_array)) )
{
// witch
// delayed deletion, to avoid potential problems with crowns not detecting
CreateTimer( WITCH_DELETE_TIME, Timer_WitchKeyDelete, entity );
SDKUnhook(entity, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch);
return;
}
}
public Action: Timer_WitchKeyDelete (Handle:timer, any:witch)
{
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", witch);
RemoveFromTrie(g_hWitchTrie, witch_key);
}
public Action: Timer_CheckRockSkeet (Handle:timer, any:rock)
{
decl rock_array[3];
decl String: rock_key[10];
FormatEx(rock_key, sizeof(rock_key), "%x", rock);
if (!GetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array)) ) { return Plugin_Continue; }
RemoveFromTrie(g_hRockTrie, rock_key);
// if rock didn't hit anyone / didn't touch anything, it was shot
if ( rock_array[rckDamage] > 0 )
{
HandleRockSkeeted( rock_array[rckSkeeter], rock_array[rckTank] );
}
return Plugin_Continue;
}
// boomer got somebody
public Action: Event_PlayerBoomed (Handle:event, const String:name[], bool:dontBroadcast)
{
new attacker = GetClientOfUserId( GetEventInt(event, "attacker") );
new bool: byBoom = GetEventBool(event, "by_boomer");
if ( byBoom && IS_VALID_INFECTED(attacker) )
{
g_bBoomerHitSomebody[attacker] = true;
// check if it was vomit spray
new bool: byExplosion = GetEventBool(event, "exploded");
if ( !byExplosion )
{
// count amount of booms
if ( !g_iBoomerVomitHits[attacker] ) {
// check for boom count later
CreateTimer( VOMIT_DURATION_TIME, Timer_BoomVomitCheck, attacker, TIMER_FLAG_NO_MAPCHANGE );
}
g_iBoomerVomitHits[attacker]++;
}
}
}
// check how many booms landed
public Action: Timer_BoomVomitCheck ( Handle:timer, any:client )
{
HandleVomitLanded( client, g_iBoomerVomitHits[client] );
g_iBoomerVomitHits[client] = 0;
}
// boomers that didn't bile anyone
public Action: Event_BoomerExploded (Handle:event, const String:name[], bool:dontBroadcast)
{
new client = GetClientOfUserId( GetEventInt(event, "userid") );
new bool: biled = GetEventBool(event, "splashedbile");
if ( !biled && !g_bBoomerHitSomebody[client] )
{
new attacker = GetClientOfUserId( GetEventInt(event, "attacker") );
if ( IS_VALID_SURVIVOR(attacker) )
{
HandlePop( attacker, client, g_iBoomerGotShoved[client], (GetGameTime() - g_fSpawnTime[client]) );
}
}
}
// crown tracking
public Action: Event_WitchSpawned ( Handle:event, const String:name[], bool:dontBroadcast )
{
new witch = GetEventInt(event, "witchid");
SDKHook(witch, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch);
new witch_dmg_array[MAXPLAYERS+DMGARRAYEXT];
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", witch);
witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth);
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false);
}
public Action: Event_WitchKilled ( Handle:event, const String:name[], bool:dontBroadcast )
{
new witch = GetEventInt(event, "witchid");
new attacker = GetClientOfUserId( GetEventInt(event, "userid") );
SDKUnhook(witch, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch);
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
new bool: bOneShot = GetEventBool(event, "oneshot");
// is it a crown / drawcrown?
new Handle: pack = CreateDataPack();
WritePackCell( pack, attacker );
WritePackCell( pack, witch );
WritePackCell( pack, (bOneShot) ? 1 : 0 );
CreateTimer( WITCH_CHECK_TIME, Timer_CheckWitchCrown, pack );
return Plugin_Continue;
}
public Action: Event_WitchHarasserSet ( Handle:event, const String:name[], bool:dontBroadcast )
{
new witch = GetEventInt(event, "witchid");
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", witch);
decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT];
if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) )
{
for ( new i = 0; i <= MAXPLAYERS; i++ )
{
witch_dmg_array[i] = 0;
}
witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth);
witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] = 1; // harasser set
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false);
}
else
{
witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] = 1; // harasser set
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true);
}
}
public Action: OnTakeDamageByWitch ( victim, &attacker, &inflictor, &Float:damage, &damagetype )
{
// if a survivor is hit by a witch, note it in the witch damage array (maxplayers+2 = 1)
if ( IS_VALID_SURVIVOR(victim) && damage > 0.0 )
{
// not a crown if witch hit anyone for > 0 damage
if ( IsWitch(attacker) )
{
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", attacker);
decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT];
if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) )
{
for ( new i = 0; i <= MAXPLAYERS; i++ )
{
witch_dmg_array[i] = 0;
}
witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth);
witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] = 1; // failed
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false);
}
else
{
witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] = 1; // failed
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true);
}
}
}
}
public OnTakeDamagePost_Witch ( victim, attacker, inflictor, Float:damage, damagetype )
{
// only called for witches, so no check required
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", victim);
decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT];
if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) )
{
for ( new i = 0; i <= MAXPLAYERS; i++ )
{
witch_dmg_array[i] = 0;
}
witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth);
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false);
}
// store damage done to witch
if ( IS_VALID_SURVIVOR(attacker) )
{
witch_dmg_array[attacker] += RoundToFloor(damage);
witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] -= RoundToFloor(damage);
// remember last shot
if ( g_fWitchShotStart[attacker] == 0.0 || (GetGameTime() - g_fWitchShotStart[attacker]) > SHOTGUN_BLAST_TIME )
{
// reset last shot damage count and attacker
g_fWitchShotStart[attacker] = GetGameTime();
witch_dmg_array[MAXPLAYERS+WTCH_CROWNER] = attacker;
witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = 0;
witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] = ( damagetype & DMG_BUCKSHOT ) ? 1 : 0; // only allow shotguns
}
// continued blast, add up
witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] += RoundToFloor(damage);
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true);
}
else
{
// store all chip from other sources than survivor in [0]
witch_dmg_array[0] += RoundToFloor(damage);
//witch_dmg_array[MAXPLAYERS+1] -= RoundToFloor(damage);
SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true);
}
}
public Action: Timer_CheckWitchCrown(Handle:timer, Handle:pack)
{
ResetPack( pack );
new attacker = ReadPackCell( pack );
new witch = ReadPackCell( pack );
new bool:bOneShot = bool:ReadPackCell( pack );
CloseHandle( pack );
CheckWitchCrown( witch, attacker, bOneShot );
}
stock CheckWitchCrown ( witch, attacker, bool: bOneShot = false )
{
decl String:witch_key[10];
FormatEx(witch_key, sizeof(witch_key), "%x", witch);
decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT];
if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) ) {
PrintDebug(0, "Witch Crown Check: Error: Trie entry missing (entity: %i, oneshot: %i)", witch, bOneShot);
return;
}
new chipDamage = 0;
new iWitchHealth = GetConVarInt(g_hCvarWitchHealth);
/*
the attacker is the last one that did damage to witch
if their damage is full damage on an unharrassed witch, it's a full crown
if their damage is full or > drawcrown_threshhold, it's a drawcrown
*/
// not a crown at all if anyone was hit, or if the killing damage wasn't a shotgun blast
// safeguard: if it was a 'oneshot' witch kill, must've been a shotgun
// this is not enough: sometimes a shotgun crown happens that is not even reported as a oneshot...
// seems like the cause is that the witch post ontakedamage is not called in time?
if ( bOneShot )
{
witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] = 1;
}
if ( witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] || !witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] )
{
PrintDebug(0, "Witch Crown Check: Failed: bungled: %i / crowntype: %i (entity: %i)",
witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH],
witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE],
witch
);
PrintDebug(1, "Witch Crown Check: Further details: attacker: %N, attacker dmg: %i, teamless dmg: %i",
attacker,
witch_dmg_array[attacker],
witch_dmg_array[0]
);
return;
}
PrintDebug(0, "Witch Crown Check: crown shot: %i, harrassed: %i (full health: %i / drawthresh: %i / oneshot %i)",
witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT],
witch_dmg_array[MAXPLAYERS+WTCH_STARTLED],
iWitchHealth,
GetConVarInt(g_hCvarDrawCrownThresh),
bOneShot
);
// full crown? unharrassed
if ( !witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] && ( bOneShot || witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] >= iWitchHealth ) )
{
// make sure that we don't count any type of chip
if ( GetConVarBool(g_hCvarHideFakeDamage) )
{
chipDamage = 0;
for ( new i = 0; i <= MAXPLAYERS; i++ )
{
if ( i == attacker ) { continue; }
chipDamage += witch_dmg_array[i];
}
witch_dmg_array[attacker] = iWitchHealth - chipDamage;
}
HandleCrown( attacker, witch_dmg_array[attacker] );
}
else if ( witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] >= GetConVarInt(g_hCvarDrawCrownThresh) )
{
// draw crown: harassed + over X damage done by one survivor -- in ONE shot
for ( new i = 0; i <= MAXPLAYERS; i++ )
{
if ( i == attacker ) {
// count any damage done before final shot as chip
chipDamage += witch_dmg_array[i] - witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT];
} else {
chipDamage += witch_dmg_array[i];
}
}
// make sure that we don't count any type of chip
if ( GetConVarBool(g_hCvarHideFakeDamage) )
{
// unlikely to happen, but if the chip was A LOT
if ( chipDamage >= iWitchHealth ) {
chipDamage = iWitchHealth - 1;
witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = 1;
}
else {
witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = iWitchHealth - chipDamage;
}
// re-check whether it qualifies as a drawcrown:
if ( witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] < GetConVarInt(g_hCvarDrawCrownThresh) ) { return; }
}
// plus, set final shot as 'damage', and the rest as chip
HandleDrawCrown( attacker, witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT], chipDamage );
}
// remove trie
}
// tank rock
public Action: TraceAttack_Rock (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup)
{
if ( IS_VALID_SURVIVOR(attacker) )
{
/*
can't really use this for precise detection, though it does
report the last shot -- the damage report is without distance falloff
*/
decl String:rock_key[10];
decl rock_array[3];
FormatEx(rock_key, sizeof(rock_key), "%x", victim);
GetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array));
rock_array[rckDamage] += RoundToFloor(damage);
rock_array[rckSkeeter] = attacker;
SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true);
}
}
public OnTouch_Rock ( entity )
{
// remember that the rock wasn't shot
decl String:rock_key[10];
FormatEx(rock_key, sizeof(rock_key), "%x", entity);
new rock_array[3];
rock_array[rckDamage] = -1;
SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true);
SDKUnhook(entity, SDKHook_Touch, OnTouch_Rock);
}
// smoker tongue cutting & self clears
public Action: Event_TonguePullStopped (Handle:event, const String:name[], bool:dontBroadcast)
{
new attacker = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
new smoker = GetClientOfUserId( GetEventInt(event, "smoker") );
new reason = GetEventInt(event, "release_type");
if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(smoker) ) { return Plugin_Continue; }
// clear check - if the smoker itself was not shoved, handle the clear
HandleClear( attacker, smoker, victim,
ZC_SMOKER,
(g_fPinTime[smoker][1] > 0.0) ? ( GetGameTime() - g_fPinTime[smoker][1]) : -1.0,
( GetGameTime() - g_fPinTime[smoker][0]),
bool:( reason != CUT_SLASH && reason != CUT_KILL )
);
if ( attacker != victim ) { return Plugin_Continue; }
if ( reason == CUT_KILL )
{
g_bSmokerClearCheck[smoker] = true;
}
else if ( g_bSmokerShoved[smoker] )
{
HandleSmokerSelfClear( attacker, smoker, true );
}
else if ( reason == CUT_SLASH ) // note: can't trust this to actually BE a slash..
{
// check weapon
decl String:weapon[32];
GetClientWeapon( attacker, weapon, 32 );
// this doesn't count the chainsaw, but that's no-skill anyway
if ( StrEqual(weapon, "weapon_melee", false) )
{
HandleTongueCut( attacker, smoker );
}
}
return Plugin_Continue;
}
public Action: Event_TongueGrab (Handle:event, const String:name[], bool:dontBroadcast)
{
new attacker = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
if ( IS_VALID_INFECTED(attacker) && IS_VALID_SURVIVOR(victim) )
{
// new pull, clean damage
g_bSmokerClearCheck[attacker] = false;
g_bSmokerShoved[attacker] = false;
g_iSmokerVictim[attacker] = victim;
g_iSmokerVictimDamage[attacker] = 0;
g_fPinTime[attacker][0] = GetGameTime();
g_fPinTime[attacker][1] = 0.0;
}
return Plugin_Continue;
}
public Action: Event_ChokeStart (Handle:event, const String:name[], bool:dontBroadcast)
{
new attacker = GetClientOfUserId( GetEventInt(event, "userid") );
if ( g_fPinTime[attacker][0] == 0.0 ) { g_fPinTime[attacker][0] = GetGameTime(); }
g_fPinTime[attacker][1] = GetGameTime();
}
public Action: Event_ChokeStop (Handle:event, const String:name[], bool:dontBroadcast)
{
new attacker = GetClientOfUserId( GetEventInt(event, "userid") );
new victim = GetClientOfUserId( GetEventInt(event, "victim") );
new smoker = GetClientOfUserId( GetEventInt(event, "smoker") );
new reason = GetEventInt(event, "release_type");
if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(smoker) ) { return; }
// if the smoker itself was not shoved, handle the clear
HandleClear( attacker, smoker, victim,
ZC_SMOKER,
(g_fPinTime[smoker][1] > 0.0) ? ( GetGameTime() - g_fPinTime[smoker][1]) : -1.0,
( GetGameTime() - g_fPinTime[smoker][0]),
bool:( reason != CUT_SLASH && reason != CUT_KILL )
);
}
// car alarm handling
public Hook_CarAlarmStart ( const String:output[], caller, activator, Float:delay )
{
//decl String:car_key[10];
//FormatEx(car_key, sizeof(car_key), "%x", entity);
PrintDebug( 0, "calarm trigger: caller %i / activator %i / delay: %.2f", caller, activator, delay );
}
public Action: Event_CarAlarmGoesOff( Handle:event, const String:name[], bool:dontBroadcast )
{
g_fLastCarAlarm = GetGameTime();
}
public Action: OnTakeDamage_Car ( victim, &attacker, &inflictor, &Float:damage, &damagetype )
{
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
/*
boomer popped on alarmed car =
DMG_BLAST_SURFACE| DMG_BLAST
and inflictor is the boomer
melee slash/club =
DMG_SLOWBURN|DMG_PREVENT_PHYSICS_FORCE + DMG_CLUB or DMG_SLASH
shove is without DMG_SLOWBURN
*/
CreateTimer( 0.01, Timer_CheckAlarm, victim, TIMER_FLAG_NO_MAPCHANGE );
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", victim);
SetTrieValue(g_hCarTrie, car_key, attacker);
if ( damagetype & DMG_BLAST )
{
if ( IS_VALID_INFECTED(inflictor) && GetEntProp(inflictor, Prop_Send, "m_zombieClass") == ZC_BOOMER ) {
g_iLastCarAlarmReason[attacker] = CALARM_BOOMER;
g_iLastCarAlarmBoomer = inflictor;
} else {
g_iLastCarAlarmReason[attacker] = CALARM_EXPLOSION;
}
}
else if ( damage == 0.0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) && !( damagetype & DMG_SLOWBURN) )
{
g_iLastCarAlarmReason[attacker] = CALARM_TOUCHED;
}
else
{
g_iLastCarAlarmReason[attacker] = CALARM_HIT;
}
return Plugin_Continue;
}
public OnTouch_Car ( entity, client )
{
if ( !IS_VALID_SURVIVOR(client) ) { return; }
CreateTimer( 0.01, Timer_CheckAlarm, entity, TIMER_FLAG_NO_MAPCHANGE );
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", entity);
SetTrieValue(g_hCarTrie, car_key, client);
g_iLastCarAlarmReason[client] = CALARM_TOUCHED;
return;
}
public Action: OnTakeDamage_CarGlass ( victim, &attacker, &inflictor, &Float:damage, &damagetype )
{
// check for either: boomer pop or survivor
if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; }
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", victim);
new parentEntity;
if ( GetTrieValue(g_hCarTrie, car_key, parentEntity) )
{
CreateTimer( 0.01, Timer_CheckAlarm, parentEntity, TIMER_FLAG_NO_MAPCHANGE );
FormatEx(car_key, sizeof(car_key), "%x", parentEntity);
SetTrieValue(g_hCarTrie, car_key, attacker);
if ( damagetype & DMG_BLAST )
{
if ( IS_VALID_INFECTED(inflictor) && GetEntProp(inflictor, Prop_Send, "m_zombieClass") == ZC_BOOMER ) {
g_iLastCarAlarmReason[attacker] = CALARM_BOOMER;
g_iLastCarAlarmBoomer = inflictor;
} else {
g_iLastCarAlarmReason[attacker] = CALARM_EXPLOSION;
}
}
else if ( damage == 0.0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) && !( damagetype & DMG_SLOWBURN) )
{
g_iLastCarAlarmReason[attacker] = CALARM_TOUCHED;
}
else
{
g_iLastCarAlarmReason[attacker] = CALARM_HIT;
}
}
return Plugin_Continue;
}
public OnTouch_CarGlass ( entity, client )
{
if ( !IS_VALID_SURVIVOR(client) ) { return; }
decl String:car_key[10];
FormatEx(car_key, sizeof(car_key), "%x", entity);
new parentEntity;
if ( GetTrieValue(g_hCarTrie, car_key, parentEntity) )
{
CreateTimer( 0.01, Timer_CheckAlarm, parentEntity, TIMER_FLAG_NO_MAPCHANGE );
FormatEx(car_key, sizeof(car_key), "%x", parentEntity);
SetTrieValue(g_hCarTrie, car_key, client);
g_iLastCarAlarmReason[client] = CALARM_TOUCHED;
}
return;
}
public Action: Timer_CheckAlarm (Handle:timer, any:entity)
{
//PrintToChatAll( "checking alarm: time: %.3f", GetGameTime() - g_fLastCarAlarm );
if ( (GetGameTime() - g_fLastCarAlarm) < CARALARM_MIN_TIME )
{
// got a match, drop stuff from trie and handle triggering
decl String:car_key[10];
new testEntity;
new survivor = -1;
// remove car glass
FormatEx(car_key, sizeof(car_key), "%x_A", entity);
if ( GetTrieValue(g_hCarTrie, car_key, testEntity) )
{
RemoveFromTrie(g_hCarTrie, car_key);
SDKUnhook(testEntity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass);
SDKUnhook(testEntity, SDKHook_Touch, OnTouch_CarGlass);
}
FormatEx(car_key, sizeof(car_key), "%x_B", entity);
if ( GetTrieValue(g_hCarTrie, car_key, testEntity) )
{
RemoveFromTrie(g_hCarTrie, car_key);
SDKUnhook(testEntity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass);
SDKUnhook(testEntity, SDKHook_Touch, OnTouch_CarGlass);
}
// remove car
FormatEx(car_key, sizeof(car_key), "%x", entity);
if ( GetTrieValue(g_hCarTrie, car_key, survivor) )
{
RemoveFromTrie(g_hCarTrie, car_key);
SDKUnhook(entity, SDKHook_OnTakeDamage, OnTakeDamage_Car);
SDKUnhook(entity, SDKHook_Touch, OnTouch_Car);
}
// check for infected assistance
new infected = 0;
if ( IS_VALID_SURVIVOR(survivor) )
{
if ( g_iLastCarAlarmReason[survivor] == CALARM_BOOMER )
{
infected = g_iLastCarAlarmBoomer;
}
else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_carryAttacker")) )
{
infected = GetEntPropEnt(survivor, Prop_Send, "m_carryAttacker");
}
else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_jockeyAttacker")) )
{
infected = GetEntPropEnt(survivor, Prop_Send, "m_jockeyAttacker");
}
else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_tongueOwner")) )
{
infected = GetEntPropEnt(survivor, Prop_Send, "m_tongueOwner");
}
}
HandleCarAlarmTriggered(
survivor,
infected,
(IS_VALID_INGAME(survivor)) ? g_iLastCarAlarmReason[survivor] : CALARM_UNKNOWN
);
}
}
/* throwactivate .. for more reliable rock-tracking?
public Action: L4D_OnCThrowActivate ( ability )
{
// tank throws rock
if ( !IsValidEntity(ability) ) { return Plugin_Continue; }
// find tank player
new tank = GetEntPropEnt(ability, Prop_Send, "m_owner");
if ( !IS_VALID_INGAME(tank) ) { return Plugin_Continue; }
...
}
*/
/*
Reporting and forwards
----------------------
*/
// boomer pop
stock HandlePop( attacker, victim, shoveCount, Float:timeAlive )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_POP )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 popped \x05%N\x01.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 popped a boomer.", attacker );
}
}
Call_StartForward(g_hForwardBoomerPop);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(shoveCount);
Call_PushFloat(timeAlive);
Call_Finish();
}
// charger level
stock HandleLevel( attacker, victim )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_LEVEL )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 leveled \x05%N\x01.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 leveled a charger.", attacker );
}
else {
PrintToChatAll( "A charger was leveled." );
}
}
// call forward
Call_StartForward(g_hForwardLevel);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
// charger level hurt
stock HandleLevelHurt( attacker, victim, damage )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_HURTLEVEL )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 chip-leveled \x05%N\x01 (\x03%i\x01 damage).", attacker, victim, damage );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 chip-leveled a charger. (\x03%i\x01 damage)", attacker, damage );
}
else {
PrintToChatAll( "A charger was chip-leveled (\x03%i\x01 damage).", damage );
}
}
// call forward
Call_StartForward(g_hForwardLevelHurt);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(damage);
Call_Finish();
}
// deadstops
stock HandleDeadstop( attacker, victim )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_DEADSTOP )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 deadstopped \x05%N\x01.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 deadstopped a hunter.", attacker );
}
}
Call_StartForward(g_hForwardHunterDeadstop);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
stock HandleShove( attacker, victim, zombieClass )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SHOVE )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 shoved \x05%N\x01.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 shoved an SI.", attacker );
}
}
Call_StartForward(g_hForwardSIShove);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(zombieClass);
Call_Finish();
}
// real skeet
stock HandleSkeet( attacker, victim, bool:bMelee = false, bool:bSniper = false, bool:bGL = false )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SKEET )
{
if ( attacker == -2 )
{
// team skeet sets to -2
if ( IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) {
PrintToChatAll( "\x05%N\x01 was team-skeeted.", victim );
} else {
PrintToChatAll( "\x01A hunter was team-skeeted." );
}
}
else if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 %sskeeted \x05%N\x01.",
attacker,
(bMelee) ? "melee-": ((bSniper) ? "headshot-" : ((bGL) ? "grenade-" : "") ),
victim
);
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 %sskeeted a hunter.",
attacker,
(bMelee) ? "melee-": ((bSniper) ? "headshot-" : ((bGL) ? "grenade-" : "") )
);
}
}
// call forward
if ( bSniper )
{
Call_StartForward(g_hForwardSkeetSniper);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
else if ( bGL )
{
Call_StartForward(g_hForwardSkeetGL);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
else if ( bMelee )
{
Call_StartForward(g_hForwardSkeetMelee);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
else
{
Call_StartForward(g_hForwardSkeet);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
}
// hurt skeet / non-skeet
// NOTE: bSniper not set yet, do this
stock HandleNonSkeet( attacker, victim, damage, bool:bOverKill = false, bool:bMelee = false, bool:bSniper = false )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_HURTSKEET )
{
if ( IS_VALID_INGAME(victim) )
{
PrintToChatAll( "\x05%N\x01 was \x04not\x01 skeeted (\x03%i\x01 damage).%s", victim, damage, (bOverKill) ? "(Would've skeeted if hunter were unchipped!)" : "" );
}
else
{
PrintToChatAll( "\x01Hunter was \x04not\x01 skeeted (\x03%i\x01 damage).%s", damage, (bOverKill) ? "(Would've skeeted if hunter were unchipped!)" : "" );
}
}
// call forward
if ( bSniper )
{
Call_StartForward(g_hForwardSkeetSniperHurt);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(damage);
Call_PushCell(bOverKill);
Call_Finish();
}
else if ( bMelee )
{
Call_StartForward(g_hForwardSkeetMeleeHurt);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(damage);
Call_PushCell(bOverKill);
Call_Finish();
}
else
{
Call_StartForward(g_hForwardSkeetHurt);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(damage);
Call_PushCell(bOverKill);
Call_Finish();
}
}
// crown
HandleCrown( attacker, damage )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_CROWN )
{
if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 crowned a witch (\x03%i\x01 damage).", attacker, damage );
}
else {
PrintToChatAll( "A witch was crowned." );
}
}
// call forward
Call_StartForward(g_hForwardCrown);
Call_PushCell(attacker);
Call_PushCell(damage);
Call_Finish();
}
// drawcrown
HandleDrawCrown( attacker, damage, chipdamage )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_DRAWCROWN )
{
if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 draw-crowned a witch (\x03%i\x01 damage, \x05%i\x01 chip).", attacker, damage, chipdamage );
}
else {
PrintToChatAll( "A witch was draw-crowned (\x03%i\x01 damage, \x05%i\x01 chip).", damage, chipdamage );
}
}
// call forward
Call_StartForward(g_hForwardDrawCrown);
Call_PushCell(attacker);
Call_PushCell(damage);
Call_PushCell(chipdamage);
Call_Finish();
}
// smoker clears
HandleTongueCut( attacker, victim )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_TONGUECUT )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 cut \x05%N\x01's tongue.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 cut smoker tongue.", attacker );
}
}
// call forward
Call_StartForward(g_hForwardTongueCut);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
HandleSmokerSelfClear( attacker, victim, bool:withShove = false )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SELFCLEAR &&
(!withShove || GetConVarInt(g_hCvarReport) & REP_SELFCLEARSHOVE )
) {
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 self-cleared from \x05%N\x01's tongue%s.", attacker, victim, (withShove) ? " by shoving" : "" );
}
else if ( IS_VALID_INGAME(attacker) )
{
PrintToChatAll( "\x04%N\x01 self-cleared from a smoker tongue%s.", attacker, (withShove) ? " by shoving" : "" );
}
}
// call forward
Call_StartForward(g_hForwardSmokerSelfClear);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(withShove);
Call_Finish();
}
// rocks
HandleRockEaten( attacker, victim )
{
Call_StartForward(g_hForwardRockEaten);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
HandleRockSkeeted( attacker, victim )
{
// report?
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_ROCKSKEET )
{
/*
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
PrintToChatAll( "\x04%N\x01 skeeted \x05%N\x01's rock.", attacker, victim );
}
else if ( IS_VALID_INGAME(attacker) )
{
}
*/
PrintToChatAll( "\x04%N\x01 skeeted a tank rock.", attacker );
}
Call_StartForward(g_hForwardRockSkeeted);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_Finish();
}
// highpounces
stock HandleHunterDP( attacker, victim, actualDamage, Float:calculatedDamage, Float:height, bool:playerIncapped = false )
{
// report?
if ( GetConVarBool(g_hCvarReport)
&& GetConVarInt(g_hCvarReportFlags) & REP_HUNTERDP
&& height >= GetConVarFloat(g_hCvarHunterDPThresh)
&& !playerIncapped
) {
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) )
{
PrintToChatAll( "\x04%N\x01 high-pounced \x05%N\x01 (\x03%i\x01 damage, height: \x05%i\x01).", attacker, victim, RoundFloat(calculatedDamage), RoundFloat(height) );
}
else if ( IS_VALID_INGAME(victim) )
{
PrintToChatAll( "A hunter high-pounced \x05%N\x01 (\x03%i\x01 damage, height: \x05%i\x01).", victim, RoundFloat(calculatedDamage), RoundFloat(height) );
}
}
Call_StartForward(g_hForwardHunterDP);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(actualDamage);
Call_PushFloat(calculatedDamage);
Call_PushFloat(height);
Call_PushCell( (height >= GetConVarFloat(g_hCvarHunterDPThresh)) ? 1 : 0 );
Call_PushCell( (playerIncapped) ? 1 : 0 );
Call_Finish();
}
stock HandleJockeyDP( attacker, victim, Float:height )
{
// report?
if ( GetConVarBool(g_hCvarReport)
&& GetConVarInt(g_hCvarReportFlags) & REP_JOCKEYDP
&& height >= GetConVarFloat(g_hCvarJockeyDPThresh)
) {
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) )
{
PrintToChatAll( "\x04%N\x01 jockey high-pounced \x05%N\x01 (height: \x05%i\x01).", attacker, victim, RoundFloat(height) );
}
else if ( IS_VALID_INGAME(victim) )
{
PrintToChatAll( "A jockey high-pounced \x05%N\x01 (height: \x05%i\x01).", victim, RoundFloat(height) );
}
}
Call_StartForward(g_hForwardJockeyDP);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushFloat(height);
Call_PushCell( (height >= GetConVarFloat(g_hCvarJockeyDPThresh)) ? 1 : 0 );
Call_Finish();
}
// deathcharges
stock HandleDeathCharge( attacker, victim, Float:height, Float:distance, bool:bCarried = true )
{
// report?
if ( GetConVarBool(g_hCvarReport) &&
GetConVarInt(g_hCvarReportFlags) & REP_DEATHCHARGE &&
height >= GetConVarFloat(g_hCvarDeathChargeHeight)
) {
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) )
{
PrintToChatAll( "\x04%N\x01 death-charged \x05%N\x01 %s(height: \x05%i\x01).",
attacker,
victim,
(bCarried) ? "" : "by bowling ",
RoundFloat(height)
);
}
else if ( IS_VALID_INGAME(victim) )
{
PrintToChatAll( "A charger death-charged \x05%N\x01 %s(height: \x05%i\x01).",
victim,
(bCarried) ? "" : "by bowling ",
RoundFloat(height)
);
}
}
Call_StartForward(g_hForwardDeathCharge);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushFloat(height);
Call_PushFloat(distance);
Call_PushCell( (bCarried) ? 1 : 0 );
Call_Finish();
}
// SI clears (cleartimeA = pummel/pounce/ride/choke, cleartimeB = tongue drag, charger carry)
stock HandleClear( attacker, victim, pinVictim, zombieClass, Float:clearTimeA, Float:clearTimeB, bool:bWithShove = false )
{
// sanity check:
if ( clearTimeA < 0 && clearTimeA != -1.0 ) { clearTimeA = 0.0; }
if ( clearTimeB < 0 && clearTimeB != -1.0 ) { clearTimeB = 0.0; }
PrintDebug(0, "Clear: %i freed %i from %i: time: %.2f / %.2f -- class: %s (with shove? %i)", attacker, pinVictim, victim, clearTimeA, clearTimeB, g_csSIClassName[zombieClass], bWithShove );
if ( attacker != pinVictim && GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_INSTACLEAR )
{
new Float: fMinTime = GetConVarFloat(g_hCvarInstaTime);
new Float: fClearTime = clearTimeA;
if ( zombieClass == ZC_CHARGER || zombieClass == ZC_SMOKER ) { fClearTime = clearTimeB; }
if ( fClearTime != -1.0 && fClearTime <= fMinTime )
{
if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) )
{
if ( IS_VALID_INGAME(pinVictim) )
{
PrintToChatAll( "\x04%N\x01 insta-cleared \x05%N\x01 from \x04%N\x01 (%s) (%.2f seconds).",
attacker, pinVictim, victim,
g_csSIClassName[zombieClass],
fClearTime
);
} else {
PrintToChatAll( "\x04%N\x01 insta-cleared a teammate from \x04%N\x01 (%s) (%.2f seconds).",
attacker, victim,
g_csSIClassName[zombieClass],
fClearTime
);
}
}
else if ( IS_VALID_INGAME(attacker) )
{
if ( IS_VALID_INGAME(pinVictim) )
{
PrintToChatAll( "\x04%N\x01 insta-cleared \x05%N\x01 from a %s (%.2f seconds).",
attacker, pinVictim,
g_csSIClassName[zombieClass],
fClearTime
);
} else {
PrintToChatAll( "\x04%N\x01 insta-cleared a teammate from a %s (%.2f seconds).",
attacker,
g_csSIClassName[zombieClass],
fClearTime
);
}
}
}
}
Call_StartForward(g_hForwardClear);
Call_PushCell(attacker);
Call_PushCell(victim);
Call_PushCell(pinVictim);
Call_PushCell(zombieClass);
Call_PushFloat(clearTimeA);
Call_PushFloat(clearTimeB);
Call_PushCell( (bWithShove) ? 1 : 0 );
Call_Finish();
}
// booms
stock HandleVomitLanded( attacker, boomCount )
{
Call_StartForward(g_hForwardVomitLanded);
Call_PushCell(attacker);
Call_PushCell(boomCount);
Call_Finish();
}
// bhaps
stock HandleBHopStreak( survivor, streak, Float: maxVelocity )
{
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_BHOPSTREAK &&
IS_VALID_INGAME(survivor) && !IsFakeClient(survivor) &&
streak >= GetConVarInt(g_hCvarBHopMinStreak)
) {
PrintToChatAll( "\x04%N\x01 got \x05%i\x01 bunnyhop%s in a row (top speed: \x05%.1f\x01).",
survivor,
streak,
( streak > 1 ) ? "s" : "",
maxVelocity
);
}
Call_StartForward(g_hForwardBHopStreak);
Call_PushCell(survivor);
Call_PushCell(streak);
Call_PushFloat(maxVelocity);
Call_Finish();
}
// car alarms
stock HandleCarAlarmTriggered( survivor, infected, reason )
{
if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_CARALARM &&
IS_VALID_INGAME(survivor) && !IsFakeClient(survivor)
) {
if ( reason == CALARM_HIT ) {
PrintToChatAll( "\x05%N\x01 triggered an alarm with a hit.", survivor );
}
else if ( reason == CALARM_TOUCHED )
{
// if a survivor touches an alarmed car, it might be due to a special infected...
if ( IS_VALID_INFECTED(infected) )
{
if ( !IsFakeClient(infected) )
{
PrintToChatAll( "\x04%N\x01 made \x05%N\x01 trigger an alarm.", infected, survivor );
}
else {
switch ( GetEntProp(infected, Prop_Send, "m_zombieClass") )
{
case ZC_SMOKER: { PrintToChatAll( "\x01A hunter made \x05%N\x01 trigger an alarm.", survivor ); }
case ZC_JOCKEY: { PrintToChatAll( "\x01A jockey made \x05%N\x01 trigger an alarm.", survivor ); }
case ZC_CHARGER: { PrintToChatAll( "\x01A charger made \x05%N\x01 trigger an alarm.", survivor ); }
default: { PrintToChatAll( "\x01A bot infected made \x05%N\x01 trigger an alarm.", survivor ); }
}
}
}
else
{
PrintToChatAll( "\x05%N\x01 touched an alarmed car.", survivor );
}
}
else if ( reason == CALARM_EXPLOSION ) {
PrintToChatAll( "\x05%N\x01 triggered an alarm with an explosion.", survivor );
}
else if ( reason == CALARM_BOOMER )
{
if ( IS_VALID_INFECTED(infected) && !IsFakeClient(infected) )
{
PrintToChatAll( "\x05%N\x01 triggered an alarm by killing a boomer \x04%N\x01.", survivor, infected );
}
else
{
PrintToChatAll( "\x05%N\x01 triggered an alarm by shooting a boomer.", survivor );
}
}
else {
PrintToChatAll( "\x05%N\x01 triggered an alarm.", survivor );
}
}
Call_StartForward(g_hForwardAlarmTriggered);
Call_PushCell(survivor);
Call_PushCell(infected);
Call_PushCell(reason);
Call_Finish();
}
// support
// -------
stock GetSurvivorPermanentHealth(client)
{
return GetEntProp(client, Prop_Send, "m_iHealth");
}
stock GetSurvivorTempHealth(client)
{
new temphp = RoundToCeil(
GetEntPropFloat(client, Prop_Send, "m_healthBuffer")
- ( (GetGameTime() - GetEntPropFloat(client, Prop_Send, "m_healthBufferTime") )
* GetConVarFloat( FindConVar("pain_pills_decay_rate")))
) - 1;
return (temphp > 0 ? temphp : 0);
}
stock Float: GetSurvivorDistance(client)
{
return L4D2Direct_GetFlowDistance(client);
}
stock ShiftTankThrower()
{
new tank = -1;
if ( !g_iRocksBeingThrownCount ) { return -1; }
tank = g_iRocksBeingThrown[0];
// shift the tank array downwards, if there are more than 1 throwers
if ( g_iRocksBeingThrownCount > 1 )
{
for ( new x = 1; x <= g_iRocksBeingThrownCount; x++ )
{
g_iRocksBeingThrown[x-1] = g_iRocksBeingThrown[x];
}
}
g_iRocksBeingThrownCount--;
return tank;
}
/* Height check..
not required now
maybe for some other 'skill'?
static Float: GetHeightAboveGround( Float:pos[3] )
{
// execute Trace straight down
new Handle:trace = TR_TraceRayFilterEx( pos, ANGLE_STRAIGHT_DOWN, MASK_SHOT, RayType_Infinite, ChargeTraceFilter );
if (!TR_DidHit(trace))
{
LogError("Tracer Bug: Trace did not hit anything...");
}
decl Float:vEnd[3];
TR_GetEndPosition(vEnd, trace); // retrieve our trace endpoint
CloseHandle(trace);
return GetVectorDistance(pos, vEnd, false);
}
public bool: ChargeTraceFilter (entity, contentsMask)
{
if ( !entity || !IsValidEntity(entity) ) // dont let WORLD, or invalid entities be hit
{
return false;
}
return true;
}
*/
stock PrintDebug(debuglevel, const String:Message[], any:... )
{
decl String:DebugBuff[256];
VFormat(DebugBuff, sizeof(DebugBuff), Message, 3);
LogMessage(DebugBuff);
}
stock bool: IsWitch(entity)
{
if ( !IsValidEntity(entity) ) { return false; }
decl String: classname[24];
new strOEC: classnameOEC;
GetEdictClassname(entity, classname, sizeof(classname));
if ( !GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC) || classnameOEC != OEC_WITCH ) { return false; }
return true;
}